@webpieces/http-routing 0.3.349 → 0.3.351
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/AuthConfig.d.ts +16 -39
- package/src/AuthConfig.js +15 -32
- package/src/AuthConfig.js.map +1 -1
- package/src/AuthHooks.d.ts +35 -0
- package/src/AuthHooks.js +42 -0
- package/src/AuthHooks.js.map +1 -0
- package/src/DefaultJwtHook.d.ts +20 -0
- package/src/DefaultJwtHook.js +59 -0
- package/src/DefaultJwtHook.js.map +1 -0
- package/src/DefaultOidcVerifier.d.ts +15 -0
- package/src/DefaultOidcVerifier.js +35 -0
- package/src/DefaultOidcVerifier.js.map +1 -0
- package/src/MethodMeta.d.ts +7 -9
- package/src/MethodMeta.js +6 -9
- package/src/MethodMeta.js.map +1 -1
- package/src/filters/AuthFilter.d.ts +17 -10
- package/src/filters/AuthFilter.js +47 -26
- package/src/filters/AuthFilter.js.map +1 -1
- package/src/index.d.ts +3 -0
- package/src/index.js +14 -3
- package/src/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-routing",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.351",
|
|
4
4
|
"description": "Decorator-based routing with auto-wiring for WebPieces",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
25
|
-
"@webpieces/core-context": "0.3.
|
|
26
|
-
"@webpieces/core-util": "0.3.
|
|
25
|
+
"@webpieces/core-context": "0.3.351",
|
|
26
|
+
"@webpieces/core-util": "0.3.351",
|
|
27
|
+
"@webpieces/gcp-identity": "0.3.351",
|
|
27
28
|
"inversify": "7.10.4",
|
|
29
|
+
"jsonwebtoken": "9.0.2",
|
|
28
30
|
"minimatch": "10.0.1"
|
|
29
31
|
}
|
|
30
32
|
}
|
package/src/AuthConfig.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContextTuple
|
|
1
|
+
import { ContextTuple } from '@webpieces/core-util';
|
|
2
2
|
/**
|
|
3
3
|
* SharedSecrets - the accepted values for ONE `@AuthSharedSecret(name)`. BOTH secret1 AND secret2
|
|
4
4
|
* are accepted — this is what makes zero-downtime ROTATION possible:
|
|
@@ -15,7 +15,7 @@ export declare class SharedSecrets {
|
|
|
15
15
|
constructor(secret1: string, secret2: string);
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* AuthValues - what {@link
|
|
18
|
+
* AuthValues - what {@link JwtHook.parseJwt} returns: the authenticated user's id + roles (used
|
|
19
19
|
* by the framework to stamp a principal and enforce @AuthJwt(...roles)) plus any extra context
|
|
20
20
|
* entries the app wants set (orgId, tenant, ...). The framework sets `entries` into RequestContext
|
|
21
21
|
* via {@link RequestContext.putHeader}. Data-only structure (a class, per the guidelines).
|
|
@@ -28,44 +28,21 @@ export declare class AuthValues {
|
|
|
28
28
|
constructor(userId: string, roles?: string[], entries?: ContextTuple[], claims?: Record<string, unknown>);
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* AuthConfig - the
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to
|
|
32
|
+
* enforce `@AuthSharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —
|
|
33
|
+
* there is no verification code here. The verification MECHANISMS are separate optional hooks the
|
|
34
|
+
* app binds when it needs them:
|
|
34
35
|
*
|
|
35
|
-
* -
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* - `parseJwt` — PLUGIN: decode/verify a user JWT into {@link AuthValues} (userId, roles,
|
|
39
|
-
* context entries). The app owns the VERIFICATION — it picks the JWT library
|
|
40
|
-
* and strategy (a static HS256 secret, RS256 + JWKS with key rotation, or a
|
|
41
|
-
* provider SDK like Firebase/Auth0/Cognito). Minting a JWT is a controller
|
|
42
|
-
* concern (login), not here.
|
|
43
|
-
* - `verifyOidc` — PLUGIN: verify a Google OIDC service-to-service token against the endpoint's
|
|
44
|
-
* caller allow-list. Fully generic — the company base wires it to
|
|
45
|
-
* @webpieces/gcp-identity once, so apps never customize OIDC.
|
|
36
|
+
* - user JWT → bind a {@link JwtHook} (parseJwt + authorizeJwt).
|
|
37
|
+
* - OIDC → bind an {@link OidcHook} to override the framework's default verifier; a server that
|
|
38
|
+
* binds nothing still verifies Google OIDC via the built-in {@link DefaultOidcVerifier}.
|
|
46
39
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* none; a non-public route with no AuthConfig (or no value/plugin for its mode) fails fast.
|
|
40
|
+
* So a zero-wiring server accepts service-to-service OIDC out of the box, and an app only binds the
|
|
41
|
+
* pieces it actually uses. This class is injected `@optional` into AuthFilter (rebindable in tests);
|
|
42
|
+
* when unbound, shared-secret endpoints simply have no accepted secret and fail fast (401).
|
|
51
43
|
*/
|
|
52
|
-
export declare
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* with zero dropped requests.
|
|
57
|
-
*/
|
|
58
|
-
abstract readonly sharedSecrets: Record<string, SharedSecrets>;
|
|
59
|
-
/** Parse a user JWT (kind:'jwt') — AUTHENTICATION only. Return who the user is, or throw. */
|
|
60
|
-
abstract parseJwt(token: string): AuthValues;
|
|
61
|
-
/** Verify a Google OIDC token from an allowed caller (kind:'oidc'); throw on failure. */
|
|
62
|
-
abstract verifyOidc(token: string, callers: string[]): Promise<void>;
|
|
63
|
-
/**
|
|
64
|
-
* AUTHORIZATION: check the authenticated user against the endpoint's {@link JwtRequirement}.
|
|
65
|
-
* DEFAULT enforces `roles` (any-of; empty = any authenticated user). OVERRIDE to enforce
|
|
66
|
-
* app-defined requirements carried by `@Auth({...})` — e.g.
|
|
67
|
-
* `if (requirement['inOrg'] && !values.claims['orgId']) throw new HttpForbiddenError(...)`.
|
|
68
|
-
* Throw HttpForbiddenError to deny; return to allow. This is the pluggable seam.
|
|
69
|
-
*/
|
|
70
|
-
authorizeJwt(values: AuthValues, requirement: JwtRequirement): void;
|
|
44
|
+
export declare class AuthConfig {
|
|
45
|
+
/** Accepted shared-secret values keyed by `@AuthSharedSecret(name)`. DEFAULT empty — pass to enable. */
|
|
46
|
+
readonly sharedSecrets: Record<string, SharedSecrets>;
|
|
47
|
+
constructor(sharedSecrets?: Record<string, SharedSecrets>);
|
|
71
48
|
}
|
package/src/AuthConfig.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AuthConfig = exports.AuthValues = exports.SharedSecrets = void 0;
|
|
4
|
-
const core_util_1 = require("@webpieces/core-util");
|
|
5
4
|
/**
|
|
6
5
|
* SharedSecrets - the accepted values for ONE `@AuthSharedSecret(name)`. BOTH secret1 AND secret2
|
|
7
6
|
* are accepted — this is what makes zero-downtime ROTATION possible:
|
|
@@ -22,7 +21,7 @@ class SharedSecrets {
|
|
|
22
21
|
}
|
|
23
22
|
exports.SharedSecrets = SharedSecrets;
|
|
24
23
|
/**
|
|
25
|
-
* AuthValues - what {@link
|
|
24
|
+
* AuthValues - what {@link JwtHook.parseJwt} returns: the authenticated user's id + roles (used
|
|
26
25
|
* by the framework to stamp a principal and enforce @AuthJwt(...roles)) plus any extra context
|
|
27
26
|
* entries the app wants set (orgId, tenant, ...). The framework sets `entries` into RequestContext
|
|
28
27
|
* via {@link RequestContext.putHeader}. Data-only structure (a class, per the guidelines).
|
|
@@ -43,40 +42,24 @@ class AuthValues {
|
|
|
43
42
|
}
|
|
44
43
|
exports.AuthValues = AuthValues;
|
|
45
44
|
/**
|
|
46
|
-
* AuthConfig - the
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to
|
|
46
|
+
* enforce `@AuthSharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —
|
|
47
|
+
* there is no verification code here. The verification MECHANISMS are separate optional hooks the
|
|
48
|
+
* app binds when it needs them:
|
|
49
49
|
*
|
|
50
|
-
* -
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* - `parseJwt` — PLUGIN: decode/verify a user JWT into {@link AuthValues} (userId, roles,
|
|
54
|
-
* context entries). The app owns the VERIFICATION — it picks the JWT library
|
|
55
|
-
* and strategy (a static HS256 secret, RS256 + JWKS with key rotation, or a
|
|
56
|
-
* provider SDK like Firebase/Auth0/Cognito). Minting a JWT is a controller
|
|
57
|
-
* concern (login), not here.
|
|
58
|
-
* - `verifyOidc` — PLUGIN: verify a Google OIDC service-to-service token against the endpoint's
|
|
59
|
-
* caller allow-list. Fully generic — the company base wires it to
|
|
60
|
-
* @webpieces/gcp-identity once, so apps never customize OIDC.
|
|
50
|
+
* - user JWT → bind a {@link JwtHook} (parseJwt + authorizeJwt).
|
|
51
|
+
* - OIDC → bind an {@link OidcHook} to override the framework's default verifier; a server that
|
|
52
|
+
* binds nothing still verifies Google OIDC via the built-in {@link DefaultOidcVerifier}.
|
|
61
53
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* none; a non-public route with no AuthConfig (or no value/plugin for its mode) fails fast.
|
|
54
|
+
* So a zero-wiring server accepts service-to-service OIDC out of the box, and an app only binds the
|
|
55
|
+
* pieces it actually uses. This class is injected `@optional` into AuthFilter (rebindable in tests);
|
|
56
|
+
* when unbound, shared-secret endpoints simply have no accepted secret and fail fast (401).
|
|
66
57
|
*/
|
|
67
58
|
class AuthConfig {
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* `if (requirement['inOrg'] && !values.claims['orgId']) throw new HttpForbiddenError(...)`.
|
|
73
|
-
* Throw HttpForbiddenError to deny; return to allow. This is the pluggable seam.
|
|
74
|
-
*/
|
|
75
|
-
authorizeJwt(values, requirement) {
|
|
76
|
-
const roles = requirement.roles ?? [];
|
|
77
|
-
if (roles.length > 0 && !roles.some((role) => values.roles.includes(role))) {
|
|
78
|
-
throw new core_util_1.HttpForbiddenError(`Endpoint requires one of roles: ${roles.join(', ')}`);
|
|
79
|
-
}
|
|
59
|
+
/** Accepted shared-secret values keyed by `@AuthSharedSecret(name)`. DEFAULT empty — pass to enable. */
|
|
60
|
+
sharedSecrets;
|
|
61
|
+
constructor(sharedSecrets = {}) {
|
|
62
|
+
this.sharedSecrets = sharedSecrets;
|
|
80
63
|
}
|
|
81
64
|
}
|
|
82
65
|
exports.AuthConfig = AuthConfig;
|
package/src/AuthConfig.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthConfig.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthConfig.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;GASG;AACH,MAAa,aAAa;IAEF;IACA;IAFpB,YACoB,OAAe,EACf,OAAe;QADf,YAAO,GAAP,OAAO,CAAQ;QACf,YAAO,GAAP,OAAO,CAAQ;IAChC,CAAC;CACP;AALD,sCAKC;AAED;;;;;GAKG;AACH,MAAa,UAAU;IAEC;IACA;IACA;IAEA;IALpB,YACoB,MAAc,EACd,QAAkB,EAAE,EACpB,UAA0B,EAAE;IAC5C,wGAAwG;IACxF,SAAkC,EAAE;QAJpC,WAAM,GAAN,MAAM,CAAQ;QACd,UAAK,GAAL,KAAK,CAAe;QACpB,YAAO,GAAP,OAAO,CAAqB;QAE5B,WAAM,GAAN,MAAM,CAA8B;IACrD,CAAC;CACP;AARD,gCAQC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAa,UAAU;IACnB,wGAAwG;IAC/F,aAAa,CAAgC;IAEtD,YAAY,gBAA+C,EAAE;QACzD,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACvC,CAAC;CACJ;AAPD,gCAOC","sourcesContent":["import { ContextTuple } from '@webpieces/core-util';\n\n/**\n * SharedSecrets - the accepted values for ONE `@AuthSharedSecret(name)`. BOTH secret1 AND secret2\n * are accepted — this is what makes zero-downtime ROTATION possible:\n *\n * to rotate: shift secret2 → secret1, and put the NEW secret in secret2. Callers cut over from\n * the old value to the new during the window; once every caller sends the new one, the stale\n * value falls out on the next shift. At all times EITHER key works, so no request is dropped.\n *\n * Data-only structure (a class, per the guidelines). Leave secret2 empty for a single secret.\n */\nexport class SharedSecrets {\n constructor(\n public readonly secret1: string,\n public readonly secret2: string,\n ) {}\n}\n\n/**\n * AuthValues - what {@link JwtHook.parseJwt} returns: the authenticated user's id + roles (used\n * by the framework to stamp a principal and enforce @AuthJwt(...roles)) plus any extra context\n * entries the app wants set (orgId, tenant, ...). The framework sets `entries` into RequestContext\n * via {@link RequestContext.putHeader}. Data-only structure (a class, per the guidelines).\n */\nexport class AuthValues {\n constructor(\n public readonly userId: string,\n public readonly roles: string[] = [],\n public readonly entries: ContextTuple[] = [],\n // webpieces-disable no-any-unknown -- raw JWT claims for app-defined authorization (inOrg, tenant, ...)\n public readonly claims: Record<string, unknown> = {},\n ) {}\n}\n\n/**\n * AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to\n * enforce `@AuthSharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —\n * there is no verification code here. The verification MECHANISMS are separate optional hooks the\n * app binds when it needs them:\n *\n * - user JWT → bind a {@link JwtHook} (parseJwt + authorizeJwt).\n * - OIDC → bind an {@link OidcHook} to override the framework's default verifier; a server that\n * binds nothing still verifies Google OIDC via the built-in {@link DefaultOidcVerifier}.\n *\n * So a zero-wiring server accepts service-to-service OIDC out of the box, and an app only binds the\n * pieces it actually uses. This class is injected `@optional` into AuthFilter (rebindable in tests);\n * when unbound, shared-secret endpoints simply have no accepted secret and fail fast (401).\n */\nexport class AuthConfig {\n /** Accepted shared-secret values keyed by `@AuthSharedSecret(name)`. DEFAULT empty — pass to enable. */\n readonly sharedSecrets: Record<string, SharedSecrets>;\n\n constructor(sharedSecrets: Record<string, SharedSecrets> = {}) {\n this.sharedSecrets = sharedSecrets;\n }\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { JwtRequirement } from '@webpieces/core-util';
|
|
2
|
+
import { AuthValues } from './AuthConfig';
|
|
3
|
+
/**
|
|
4
|
+
* JwtHook - the OPTIONAL user-JWT mechanism. Bind one (inject by type, per no-symbol-di-tokens;
|
|
5
|
+
* rebindable in tests) to turn on `@AuthJwt(...)` endpoints. When NO JwtHook is bound, the framework
|
|
6
|
+
* {@link AuthFilter} treats every jwt endpoint as "not enabled" and fails fast (401) — there is no
|
|
7
|
+
* default JWT verification because it needs an app secret + payload shape the framework can't guess.
|
|
8
|
+
*
|
|
9
|
+
* - `parseJwt` — AUTHENTICATION: decode/verify a user JWT into {@link AuthValues}, or throw. The
|
|
10
|
+
* app owns the strategy (HS256 secret, RS256 + JWKS, a provider SDK, ...).
|
|
11
|
+
* - `authorizeJwt` — AUTHORIZATION: check the authenticated user against the endpoint's
|
|
12
|
+
* {@link JwtRequirement}. The DEFAULT enforces `roles` (any-of; empty = any
|
|
13
|
+
* authenticated user); override for app-defined requirements carried by
|
|
14
|
+
* `@Auth({...})` — e.g. `if (requirement['inOrg'] && !values.claims['orgId']) ...`.
|
|
15
|
+
*/
|
|
16
|
+
export declare abstract class JwtHook {
|
|
17
|
+
/** Parse a user JWT (kind:'jwt') — AUTHENTICATION only. Return who the user is, or throw. */
|
|
18
|
+
abstract parseJwt(token: string): AuthValues;
|
|
19
|
+
/**
|
|
20
|
+
* DEFAULT authorization: enforce `roles` (any-of; empty = any authenticated user). Override to
|
|
21
|
+
* enforce app-defined requirements. Throw HttpForbiddenError to deny; return to allow.
|
|
22
|
+
*/
|
|
23
|
+
authorizeJwt(values: AuthValues, requirement: JwtRequirement): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* OidcHook - the OPTIONAL override for Google OIDC service-to-service verification. Bind one (inject
|
|
27
|
+
* by type; rebindable in tests) ONLY to customize the caller policy — e.g. an app that reads an
|
|
28
|
+
* `ALLOWED_OIDC_CALLERS` env var at its composition root and enforces that allow-list. When NO
|
|
29
|
+
* OidcHook is bound, the framework {@link AuthFilter} runs the built-in {@link DefaultOidcVerifier}
|
|
30
|
+
* directly, so a server that wires nothing still verifies Google OIDC from its `@AuthOidc(...callers)`
|
|
31
|
+
* (else `['self']`). `verifyOidc` verifies the token against `callers`; throw on failure.
|
|
32
|
+
*/
|
|
33
|
+
export declare abstract class OidcHook {
|
|
34
|
+
abstract verifyOidc(token: string, callers: string[]): Promise<void>;
|
|
35
|
+
}
|
package/src/AuthHooks.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OidcHook = exports.JwtHook = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
/**
|
|
6
|
+
* JwtHook - the OPTIONAL user-JWT mechanism. Bind one (inject by type, per no-symbol-di-tokens;
|
|
7
|
+
* rebindable in tests) to turn on `@AuthJwt(...)` endpoints. When NO JwtHook is bound, the framework
|
|
8
|
+
* {@link AuthFilter} treats every jwt endpoint as "not enabled" and fails fast (401) — there is no
|
|
9
|
+
* default JWT verification because it needs an app secret + payload shape the framework can't guess.
|
|
10
|
+
*
|
|
11
|
+
* - `parseJwt` — AUTHENTICATION: decode/verify a user JWT into {@link AuthValues}, or throw. The
|
|
12
|
+
* app owns the strategy (HS256 secret, RS256 + JWKS, a provider SDK, ...).
|
|
13
|
+
* - `authorizeJwt` — AUTHORIZATION: check the authenticated user against the endpoint's
|
|
14
|
+
* {@link JwtRequirement}. The DEFAULT enforces `roles` (any-of; empty = any
|
|
15
|
+
* authenticated user); override for app-defined requirements carried by
|
|
16
|
+
* `@Auth({...})` — e.g. `if (requirement['inOrg'] && !values.claims['orgId']) ...`.
|
|
17
|
+
*/
|
|
18
|
+
class JwtHook {
|
|
19
|
+
/**
|
|
20
|
+
* DEFAULT authorization: enforce `roles` (any-of; empty = any authenticated user). Override to
|
|
21
|
+
* enforce app-defined requirements. Throw HttpForbiddenError to deny; return to allow.
|
|
22
|
+
*/
|
|
23
|
+
authorizeJwt(values, requirement) {
|
|
24
|
+
const roles = requirement.roles ?? [];
|
|
25
|
+
if (roles.length > 0 && !roles.some((role) => values.roles.includes(role))) {
|
|
26
|
+
throw new core_util_1.HttpForbiddenError(`Endpoint requires one of roles: ${roles.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.JwtHook = JwtHook;
|
|
31
|
+
/**
|
|
32
|
+
* OidcHook - the OPTIONAL override for Google OIDC service-to-service verification. Bind one (inject
|
|
33
|
+
* by type; rebindable in tests) ONLY to customize the caller policy — e.g. an app that reads an
|
|
34
|
+
* `ALLOWED_OIDC_CALLERS` env var at its composition root and enforces that allow-list. When NO
|
|
35
|
+
* OidcHook is bound, the framework {@link AuthFilter} runs the built-in {@link DefaultOidcVerifier}
|
|
36
|
+
* directly, so a server that wires nothing still verifies Google OIDC from its `@AuthOidc(...callers)`
|
|
37
|
+
* (else `['self']`). `verifyOidc` verifies the token against `callers`; throw on failure.
|
|
38
|
+
*/
|
|
39
|
+
class OidcHook {
|
|
40
|
+
}
|
|
41
|
+
exports.OidcHook = OidcHook;
|
|
42
|
+
//# sourceMappingURL=AuthHooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthHooks.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooks.ts"],"names":[],"mappings":";;;AAAA,oDAA0E;AAG1E;;;;;;;;;;;;GAYG;AACH,MAAsB,OAAO;IAIzB;;;OAGG;IACH,YAAY,CAAC,MAAkB,EAAE,WAA2B;QACxD,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACjF,MAAM,IAAI,8BAAkB,CAAC,mCAAmC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxF,CAAC;IACL,CAAC;CACJ;AAdD,0BAcC;AAED;;;;;;;GAOG;AACH,MAAsB,QAAQ;CAE7B;AAFD,4BAEC","sourcesContent":["import { JwtRequirement, HttpForbiddenError } from '@webpieces/core-util';\nimport { AuthValues } from './AuthConfig';\n\n/**\n * JwtHook - the OPTIONAL user-JWT mechanism. Bind one (inject by type, per no-symbol-di-tokens;\n * rebindable in tests) to turn on `@AuthJwt(...)` endpoints. When NO JwtHook is bound, the framework\n * {@link AuthFilter} treats every jwt endpoint as \"not enabled\" and fails fast (401) — there is no\n * default JWT verification because it needs an app secret + payload shape the framework can't guess.\n *\n * - `parseJwt` — AUTHENTICATION: decode/verify a user JWT into {@link AuthValues}, or throw. The\n * app owns the strategy (HS256 secret, RS256 + JWKS, a provider SDK, ...).\n * - `authorizeJwt` — AUTHORIZATION: check the authenticated user against the endpoint's\n * {@link JwtRequirement}. The DEFAULT enforces `roles` (any-of; empty = any\n * authenticated user); override for app-defined requirements carried by\n * `@Auth({...})` — e.g. `if (requirement['inOrg'] && !values.claims['orgId']) ...`.\n */\nexport abstract class JwtHook {\n /** Parse a user JWT (kind:'jwt') — AUTHENTICATION only. Return who the user is, or throw. */\n abstract parseJwt(token: string): AuthValues;\n\n /**\n * DEFAULT authorization: enforce `roles` (any-of; empty = any authenticated user). Override to\n * enforce app-defined requirements. Throw HttpForbiddenError to deny; return to allow.\n */\n authorizeJwt(values: AuthValues, requirement: JwtRequirement): void {\n const roles = requirement.roles ?? [];\n if (roles.length > 0 && !roles.some((role: string) => values.roles.includes(role))) {\n throw new HttpForbiddenError(`Endpoint requires one of roles: ${roles.join(', ')}`);\n }\n }\n}\n\n/**\n * OidcHook - the OPTIONAL override for Google OIDC service-to-service verification. Bind one (inject\n * by type; rebindable in tests) 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 from its `@AuthOidc(...callers)`\n * (else `['self']`). `verifyOidc` verifies the token against `callers`; throw on failure.\n */\nexport abstract class OidcHook {\n abstract verifyOidc(token: string, callers: string[]): Promise<void>;\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { JwtHook } from './AuthHooks';
|
|
2
|
+
import { AuthValues } from './AuthConfig';
|
|
3
|
+
/**
|
|
4
|
+
* DefaultJwtHook - a batteries-included {@link JwtHook} for the common case: HS256 user JWTs signed
|
|
5
|
+
* with ONE shared secret. Construct it with the secret and bind it — `new DefaultJwtHook(secret)` —
|
|
6
|
+
* and `@AuthJwt` endpoints work with NO custom verification code.
|
|
7
|
+
*
|
|
8
|
+
* `parseJwt` verifies the signature + expiry (jsonwebtoken, HS256 only) and maps standard claims:
|
|
9
|
+
* `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. `authorizeJwt`
|
|
10
|
+
* (role enforcement) is inherited from JwtHook. For RS256 + JWKS, a provider SDK, or a non-standard
|
|
11
|
+
* payload, write your own JwtHook subclass instead.
|
|
12
|
+
*/
|
|
13
|
+
export declare class DefaultJwtHook extends JwtHook {
|
|
14
|
+
private readonly secret;
|
|
15
|
+
constructor(secret: string);
|
|
16
|
+
parseJwt(token: string): AuthValues;
|
|
17
|
+
/** Verify HS256 signature + expiry; translate jsonwebtoken's raw error into a framework 401. */
|
|
18
|
+
private verifyToken;
|
|
19
|
+
private extractRoles;
|
|
20
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DefaultJwtHook = void 0;
|
|
4
|
+
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
5
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
6
|
+
const AuthHooks_1 = require("./AuthHooks");
|
|
7
|
+
const AuthConfig_1 = require("./AuthConfig");
|
|
8
|
+
/**
|
|
9
|
+
* DefaultJwtHook - a batteries-included {@link JwtHook} for the common case: HS256 user JWTs signed
|
|
10
|
+
* with ONE shared secret. Construct it with the secret and bind it — `new DefaultJwtHook(secret)` —
|
|
11
|
+
* and `@AuthJwt` endpoints work with NO custom verification code.
|
|
12
|
+
*
|
|
13
|
+
* `parseJwt` verifies the signature + expiry (jsonwebtoken, HS256 only) and maps standard claims:
|
|
14
|
+
* `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. `authorizeJwt`
|
|
15
|
+
* (role enforcement) is inherited from JwtHook. For RS256 + JWKS, a provider SDK, or a non-standard
|
|
16
|
+
* payload, write your own JwtHook subclass instead.
|
|
17
|
+
*/
|
|
18
|
+
class DefaultJwtHook extends AuthHooks_1.JwtHook {
|
|
19
|
+
secret;
|
|
20
|
+
constructor(secret) {
|
|
21
|
+
super();
|
|
22
|
+
this.secret = secret;
|
|
23
|
+
}
|
|
24
|
+
parseJwt(token) {
|
|
25
|
+
const payload = this.verifyToken(token);
|
|
26
|
+
const userId = payload.sub;
|
|
27
|
+
if (!userId) {
|
|
28
|
+
throw new core_util_1.HttpUnauthorizedError('JWT is missing the required "sub" (subject) claim');
|
|
29
|
+
}
|
|
30
|
+
return new AuthConfig_1.AuthValues(userId, this.extractRoles(payload), [], payload);
|
|
31
|
+
}
|
|
32
|
+
/** Verify HS256 signature + expiry; translate jsonwebtoken's raw error into a framework 401. */
|
|
33
|
+
verifyToken(token) {
|
|
34
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- AUTH TRANSLATION CHOKEPOINT: jsonwebtoken.verify throws on a bad/expired token; that must surface as a 401 Unauthorized, not bubble to the global handler as a 500. The original error is chained via cause.
|
|
35
|
+
try {
|
|
36
|
+
const decoded = (0, jsonwebtoken_1.verify)(token, this.secret, { algorithms: ['HS256'] });
|
|
37
|
+
if (typeof decoded === 'string') {
|
|
38
|
+
throw new core_util_1.HttpUnauthorizedError('JWT payload must be a JSON object, not a string');
|
|
39
|
+
}
|
|
40
|
+
return decoded;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const error = (0, core_util_1.toError)(err);
|
|
44
|
+
if (error instanceof core_util_1.HttpUnauthorizedError) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
throw new core_util_1.HttpUnauthorizedError('JWT verification failed', undefined, error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
extractRoles(payload) {
|
|
51
|
+
const roles = payload['roles'];
|
|
52
|
+
if (Array.isArray(roles)) {
|
|
53
|
+
return roles.filter((role) => typeof role === 'string');
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.DefaultJwtHook = DefaultJwtHook;
|
|
59
|
+
//# sourceMappingURL=DefaultJwtHook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DefaultJwtHook.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/DefaultJwtHook.ts"],"names":[],"mappings":";;;AAAA,+CAAkD;AAClD,oDAAsE;AACtE,2CAAsC;AACtC,6CAA0C;AAE1C;;;;;;;;;GASG;AACH,MAAa,cAAe,SAAQ,mBAAO;IACtB,MAAM,CAAS;IAEhC,YAAY,MAAc;QACtB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAEQ,QAAQ,CAAC,KAAa;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;QAC3B,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,IAAI,iCAAqB,CAAC,mDAAmD,CAAC,CAAC;QACzF,CAAC;QACD,OAAO,IAAI,uBAAU,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;IAC3E,CAAC;IAED,gGAAgG;IACxF,WAAW,CAAC,KAAa;QAC7B,8QAA8Q;QAC9Q,IAAI,CAAC;YACD,MAAM,OAAO,GAAG,IAAA,qBAAM,EAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACtE,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAC9B,MAAM,IAAI,iCAAqB,CAAC,iDAAiD,CAAC,CAAC;YACvF,CAAC;YACD,OAAO,OAAO,CAAC;QACnB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,KAAK,YAAY,iCAAqB,EAAE,CAAC;gBACzC,MAAM,KAAK,CAAC;YAChB,CAAC;YACD,MAAM,IAAI,iCAAqB,CAAC,yBAAyB,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QACjF,CAAC;IACL,CAAC;IAEO,YAAY,CAAC,OAAmB;QACpC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC;QACpE,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;CACJ;AA1CD,wCA0CC","sourcesContent":["import { verify, JwtPayload } from 'jsonwebtoken';\nimport { HttpUnauthorizedError, toError } from '@webpieces/core-util';\nimport { JwtHook } from './AuthHooks';\nimport { AuthValues } from './AuthConfig';\n\n/**\n * DefaultJwtHook - a batteries-included {@link JwtHook} for the common case: HS256 user JWTs signed\n * with ONE shared secret. Construct it with the secret and bind it — `new DefaultJwtHook(secret)` —\n * and `@AuthJwt` endpoints work with NO custom verification code.\n *\n * `parseJwt` verifies the signature + expiry (jsonwebtoken, HS256 only) and maps standard claims:\n * `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. `authorizeJwt`\n * (role enforcement) is inherited from JwtHook. For RS256 + JWKS, a provider SDK, or a non-standard\n * payload, write your own JwtHook subclass instead.\n */\nexport class DefaultJwtHook extends JwtHook {\n private readonly secret: string;\n\n constructor(secret: string) {\n super();\n this.secret = secret;\n }\n\n override parseJwt(token: string): AuthValues {\n const payload = this.verifyToken(token);\n const userId = payload.sub;\n if (!userId) {\n throw new HttpUnauthorizedError('JWT is missing the required \"sub\" (subject) claim');\n }\n return new AuthValues(userId, this.extractRoles(payload), [], payload);\n }\n\n /** Verify HS256 signature + expiry; translate jsonwebtoken's raw error into a framework 401. */\n private verifyToken(token: string): JwtPayload {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- AUTH TRANSLATION CHOKEPOINT: jsonwebtoken.verify throws on a bad/expired token; that must surface as a 401 Unauthorized, not bubble to the global handler as a 500. The original error is chained via cause.\n try {\n const decoded = verify(token, this.secret, { algorithms: ['HS256'] });\n if (typeof decoded === 'string') {\n throw new HttpUnauthorizedError('JWT payload must be a JSON object, not a string');\n }\n return decoded;\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof HttpUnauthorizedError) {\n throw error;\n }\n throw new HttpUnauthorizedError('JWT verification failed', undefined, error);\n }\n }\n\n private extractRoles(payload: JwtPayload): string[] {\n const roles = payload['roles'];\n if (Array.isArray(roles)) {\n return roles.filter((role: string) => typeof role === 'string');\n }\n return [];\n }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DefaultOidcVerifier - the framework's built-in Google OIDC verifier, injected into
|
|
3
|
+
* {@link AuthFilter} and run directly whenever no app {@link OidcHook} is bound. It is what makes
|
|
4
|
+
* OIDC "just work" with ZERO wiring: http-routing depends on @webpieces/gcp-identity ON PURPOSE so a
|
|
5
|
+
* server that binds nothing still verifies service-to-service OIDC.
|
|
6
|
+
*
|
|
7
|
+
* `verify` checks the token against the endpoint's `@AuthOidc(...callers)` allow-list, falling back
|
|
8
|
+
* to `['self']` (this service's own runtime SA) when the endpoint named none. Off-GCP, gcp-identity
|
|
9
|
+
* mints + accepts a dev token so local dev needs no GCP. Framework code reads NO process.env — an app
|
|
10
|
+
* that wants an env-driven allow-list binds an {@link OidcHook} instead (env read at its composition
|
|
11
|
+
* root), which keeps this default env-free and tests parallel-safe.
|
|
12
|
+
*/
|
|
13
|
+
export declare class DefaultOidcVerifier {
|
|
14
|
+
verify(token: string, callers: string[]): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DefaultOidcVerifier = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
7
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
8
|
+
const gcp_identity_1 = require("@webpieces/gcp-identity");
|
|
9
|
+
/**
|
|
10
|
+
* DefaultOidcVerifier - the framework's built-in Google OIDC verifier, injected into
|
|
11
|
+
* {@link AuthFilter} and run directly whenever no app {@link OidcHook} is bound. It is what makes
|
|
12
|
+
* OIDC "just work" with ZERO wiring: http-routing depends on @webpieces/gcp-identity ON PURPOSE so a
|
|
13
|
+
* server that binds nothing still verifies service-to-service OIDC.
|
|
14
|
+
*
|
|
15
|
+
* `verify` checks the token against the endpoint's `@AuthOidc(...callers)` allow-list, falling back
|
|
16
|
+
* to `['self']` (this service's own runtime SA) when the endpoint named none. Off-GCP, gcp-identity
|
|
17
|
+
* mints + accepts a dev token so local dev needs no GCP. Framework code reads NO process.env — an app
|
|
18
|
+
* that wants an env-driven allow-list binds an {@link OidcHook} instead (env read at its composition
|
|
19
|
+
* root), which keeps this default env-free and tests parallel-safe.
|
|
20
|
+
*/
|
|
21
|
+
let DefaultOidcVerifier = class DefaultOidcVerifier {
|
|
22
|
+
async verify(token, callers) {
|
|
23
|
+
const allow = callers.length > 0 ? callers : ['self'];
|
|
24
|
+
const result = await (0, gcp_identity_1.verifyOidcFromCallers)(token, allow);
|
|
25
|
+
if (!result.ok) {
|
|
26
|
+
throw new core_util_1.HttpUnauthorizedError(`OIDC rejected: ${result.reason ?? 'not an allowed caller'}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
exports.DefaultOidcVerifier = DefaultOidcVerifier;
|
|
31
|
+
exports.DefaultOidcVerifier = DefaultOidcVerifier = tslib_1.__decorate([
|
|
32
|
+
(0, core_context_1.provideFrameworkSingleton)(),
|
|
33
|
+
(0, inversify_1.injectable)()
|
|
34
|
+
], DefaultOidcVerifier);
|
|
35
|
+
//# sourceMappingURL=DefaultOidcVerifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DefaultOidcVerifier.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/DefaultOidcVerifier.ts"],"names":[],"mappings":";;;;AAAA,yCAAuC;AACvC,0DAAoE;AACpE,oDAA6D;AAC7D,0DAAgE;AAEhE;;;;;;;;;;;GAWG;AAGI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC5B,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,OAAiB;QACzC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACtD,MAAM,MAAM,GAAG,MAAM,IAAA,oCAAqB,EAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACzD,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACb,MAAM,IAAI,iCAAqB,CAAC,kBAAkB,MAAM,CAAC,MAAM,IAAI,uBAAuB,EAAE,CAAC,CAAC;QAClG,CAAC;IACL,CAAC;CACJ,CAAA;AARY,kDAAmB;8BAAnB,mBAAmB;IAF/B,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;GACA,mBAAmB,CAQ/B","sourcesContent":["import { injectable } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { HttpUnauthorizedError } from '@webpieces/core-util';\nimport { verifyOidcFromCallers } from '@webpieces/gcp-identity';\n\n/**\n * DefaultOidcVerifier - the framework's built-in Google OIDC verifier, injected into\n * {@link AuthFilter} and run directly whenever no app {@link OidcHook} is bound. It is what makes\n * OIDC \"just work\" with ZERO wiring: http-routing depends on @webpieces/gcp-identity ON PURPOSE so a\n * server that binds nothing still verifies service-to-service OIDC.\n *\n * `verify` checks the token against the endpoint's `@AuthOidc(...callers)` allow-list, falling back\n * to `['self']` (this service's own runtime SA) when the endpoint named none. Off-GCP, gcp-identity\n * mints + accepts a dev token so local dev needs no GCP. Framework code reads NO process.env — an app\n * that wants an env-driven allow-list binds an {@link OidcHook} instead (env read at its composition\n * root), which keeps this default env-free and tests parallel-safe.\n */\n@provideFrameworkSingleton()\n@injectable()\nexport class DefaultOidcVerifier {\n async verify(token: string, callers: string[]): Promise<void> {\n const allow = callers.length > 0 ? callers : ['self'];\n const result = await verifyOidcFromCallers(token, allow);\n if (!result.ok) {\n throw new HttpUnauthorizedError(`OIDC rejected: ${result.reason ?? 'not an allowed caller'}`);\n }\n }\n}\n"]}
|
package/src/MethodMeta.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RouteMetadata
|
|
1
|
+
import { RouteMetadata } from '@webpieces/core-util';
|
|
2
2
|
/**
|
|
3
3
|
* Metadata about the method being invoked.
|
|
4
4
|
* Passed to filters and contains request information.
|
|
@@ -13,10 +13,13 @@ import { RouteMetadata, AuthMeta } from '@webpieces/core-util';
|
|
|
13
13
|
* in any express dependency.
|
|
14
14
|
*
|
|
15
15
|
* Fields:
|
|
16
|
-
* - routeMeta: Static route information (httpMethod, path, methodName)
|
|
16
|
+
* - routeMeta: Static route information (httpMethod, path, methodName, authMeta)
|
|
17
17
|
* - requestDto: The deserialized request body
|
|
18
|
-
* - authMeta: Auth mode from @Authentication/@AuthOidc/... decorators
|
|
19
18
|
* - metadata: Request-scoped data for filters to communicate
|
|
19
|
+
*
|
|
20
|
+
* Auth mode lives on {@link RouteMetadata.authMeta} (the one source of truth for fixed route
|
|
21
|
+
* metadata); read it via `methodMeta.routeMeta.authMeta`. MethodMeta itself carries only the
|
|
22
|
+
* static route reference + the request-scoped body/bag.
|
|
20
23
|
*/
|
|
21
24
|
export declare class MethodMeta {
|
|
22
25
|
/**
|
|
@@ -27,17 +30,12 @@ export declare class MethodMeta {
|
|
|
27
30
|
* The deserialized request DTO.
|
|
28
31
|
*/
|
|
29
32
|
requestDto?: unknown;
|
|
30
|
-
/**
|
|
31
|
-
* Auth metadata from @Public/@Authenticated/@Roles decorators.
|
|
32
|
-
* Populated by ApiRoutingFactory so filters can read auth requirements.
|
|
33
|
-
*/
|
|
34
|
-
authMeta?: AuthMeta;
|
|
35
33
|
/**
|
|
36
34
|
* Additional metadata for storing request-scoped data.
|
|
37
35
|
* Used by filters to pass data to other filters/controllers.
|
|
38
36
|
*/
|
|
39
37
|
metadata: Map<string, unknown>;
|
|
40
|
-
constructor(routeMeta: RouteMetadata, requestDto?: unknown, metadata?: Map<string, unknown
|
|
38
|
+
constructor(routeMeta: RouteMetadata, requestDto?: unknown, metadata?: Map<string, unknown>);
|
|
41
39
|
/**
|
|
42
40
|
* Get the HTTP method (convenience accessor).
|
|
43
41
|
*/
|
package/src/MethodMeta.js
CHANGED
|
@@ -15,10 +15,13 @@ exports.MethodMeta = void 0;
|
|
|
15
15
|
* in any express dependency.
|
|
16
16
|
*
|
|
17
17
|
* Fields:
|
|
18
|
-
* - routeMeta: Static route information (httpMethod, path, methodName)
|
|
18
|
+
* - routeMeta: Static route information (httpMethod, path, methodName, authMeta)
|
|
19
19
|
* - requestDto: The deserialized request body
|
|
20
|
-
* - authMeta: Auth mode from @Authentication/@AuthOidc/... decorators
|
|
21
20
|
* - metadata: Request-scoped data for filters to communicate
|
|
21
|
+
*
|
|
22
|
+
* Auth mode lives on {@link RouteMetadata.authMeta} (the one source of truth for fixed route
|
|
23
|
+
* metadata); read it via `methodMeta.routeMeta.authMeta`. MethodMeta itself carries only the
|
|
24
|
+
* static route reference + the request-scoped body/bag.
|
|
22
25
|
*/
|
|
23
26
|
class MethodMeta {
|
|
24
27
|
/**
|
|
@@ -30,11 +33,6 @@ class MethodMeta {
|
|
|
30
33
|
*/
|
|
31
34
|
// webpieces-disable no-any-unknown -- request DTO type is erased at the filter boundary
|
|
32
35
|
requestDto;
|
|
33
|
-
/**
|
|
34
|
-
* Auth metadata from @Public/@Authenticated/@Roles decorators.
|
|
35
|
-
* Populated by ApiRoutingFactory so filters can read auth requirements.
|
|
36
|
-
*/
|
|
37
|
-
authMeta;
|
|
38
36
|
/**
|
|
39
37
|
* Additional metadata for storing request-scoped data.
|
|
40
38
|
* Used by filters to pass data to other filters/controllers.
|
|
@@ -45,11 +43,10 @@ class MethodMeta {
|
|
|
45
43
|
// webpieces-disable no-any-unknown -- request DTO type is erased at the filter boundary
|
|
46
44
|
requestDto,
|
|
47
45
|
// webpieces-disable no-any-unknown -- request-scoped bag holds heterogeneous filter data
|
|
48
|
-
metadata
|
|
46
|
+
metadata) {
|
|
49
47
|
this.routeMeta = routeMeta;
|
|
50
48
|
this.requestDto = requestDto;
|
|
51
49
|
this.metadata = metadata ?? new Map();
|
|
52
|
-
this.authMeta = authMeta ?? routeMeta.authMeta;
|
|
53
50
|
}
|
|
54
51
|
/**
|
|
55
52
|
* Get the HTTP method (convenience accessor).
|
package/src/MethodMeta.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MethodMeta.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/MethodMeta.ts"],"names":[],"mappings":";;;AAEA
|
|
1
|
+
{"version":3,"file":"MethodMeta.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/MethodMeta.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,UAAU;IACnB;;OAEG;IACH,SAAS,CAAgB;IAEzB;;OAEG;IACH,wFAAwF;IACxF,UAAU,CAAW;IAErB;;;OAGG;IACH,yFAAyF;IACzF,QAAQ,CAAuB;IAE/B,YACI,SAAwB;IACxB,wFAAwF;IACxF,UAAoB;IACpB,yFAAyF;IACzF,QAA+B;QAE/B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,IAAI,IAAI,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,IAAI,UAAU;QACV,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;IACrC,CAAC;IAED;;OAEG;IACH,IAAI,IAAI;QACJ,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,IAAI,UAAU;QACV,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;IACrC,CAAC;CACJ;AAnDD,gCAmDC","sourcesContent":["import { RouteMetadata } from '@webpieces/core-util';\n\n/**\n * Metadata about the method being invoked.\n * Passed to filters and contains request information.\n *\n * MethodMeta is DTO-only - it does NOT contain Express req/res, nor the raw headers. The raw\n * inbound request (headers/method/path) lives on the transport-neutral {@link HttpRequest} in\n * RequestContext (read via `RequestContext.getRequest()`); MethodMeta carries only the typed\n * body + route/auth metadata that flow as the chain's call argument.\n *\n * It is the meta type every `Filter<MethodMeta, …>` is parameterized over. It lives in\n * @webpieces/http-routing and is express-free, so filter authors reference it without pulling\n * in any express dependency.\n *\n * Fields:\n * - routeMeta: Static route information (httpMethod, path, methodName, authMeta)\n * - requestDto: The deserialized request body\n * - metadata: Request-scoped data for filters to communicate\n *\n * Auth mode lives on {@link RouteMetadata.authMeta} (the one source of truth for fixed route\n * metadata); read it via `methodMeta.routeMeta.authMeta`. MethodMeta itself carries only the\n * static route reference + the request-scoped body/bag.\n */\nexport class MethodMeta {\n /**\n * Route metadata (httpMethod, path, methodName, parameterTypes)\n */\n routeMeta: RouteMetadata;\n\n /**\n * The deserialized request DTO.\n */\n // webpieces-disable no-any-unknown -- request DTO type is erased at the filter boundary\n requestDto?: unknown;\n\n /**\n * Additional metadata for storing request-scoped data.\n * Used by filters to pass data to other filters/controllers.\n */\n // webpieces-disable no-any-unknown -- request-scoped bag holds heterogeneous filter data\n metadata: Map<string, unknown>;\n\n constructor(\n routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- request DTO type is erased at the filter boundary\n requestDto?: unknown,\n // webpieces-disable no-any-unknown -- request-scoped bag holds heterogeneous filter data\n metadata?: Map<string, unknown>,\n ) {\n this.routeMeta = routeMeta;\n this.requestDto = requestDto;\n this.metadata = metadata ?? new Map();\n }\n\n /**\n * Get the HTTP method (convenience accessor).\n */\n get httpMethod(): string {\n return this.routeMeta.httpMethod;\n }\n\n /**\n * Get the request path (convenience accessor).\n */\n get path(): string {\n return this.routeMeta.path;\n }\n\n /**\n * Get the method name (convenience accessor).\n */\n get methodName(): string {\n return this.routeMeta.methodName;\n }\n}\n"]}
|
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import { Filter, WpResponse, Service } from '../Filter';
|
|
2
2
|
import { MethodMeta } from '../MethodMeta';
|
|
3
3
|
import { AuthConfig } from '../AuthConfig';
|
|
4
|
+
import { JwtHook, OidcHook } from '../AuthHooks';
|
|
5
|
+
import { DefaultOidcVerifier } from '../DefaultOidcVerifier';
|
|
4
6
|
/**
|
|
5
7
|
* AuthFilter - the ONE framework auth filter, auto-installed just below the error filter on every
|
|
6
8
|
* route. It is TRANSPORT-NEUTRAL: it reads the raw credential from the {@link HttpRequest} in
|
|
7
9
|
* RequestContext (never express), so the SAME check runs over HTTP and via createApiClient.
|
|
8
10
|
*
|
|
9
|
-
* It enforces the endpoint's AuthMode
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
11
|
+
* It enforces the endpoint's AuthMode from separately-bound pieces, each OPTIONAL except the OIDC
|
|
12
|
+
* default:
|
|
13
|
+
* - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). No
|
|
14
|
+
* AuthConfig bound → no accepted secret → fail fast (401).
|
|
15
|
+
* - jwt → the bound {@link JwtHook} (`parseJwt` + `authorizeJwt`). No JwtHook bound →
|
|
16
|
+
* "not enabled" (401): JWT needs an app secret + payload shape.
|
|
17
|
+
* - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}
|
|
18
|
+
* run DIRECTLY — so a server that wires NOTHING still verifies Google OIDC.
|
|
19
|
+
* - public → BEST-EFFORT jwt parse (only if a JwtHook is bound): stamp the user's context so
|
|
20
|
+
* a logged-out page still knows who is logged in; never fails.
|
|
15
21
|
*
|
|
16
|
-
*
|
|
17
|
-
* jsonwebtoken / gcp-identity.
|
|
22
|
+
* Zero wiring = OIDC just works; an app only binds the hooks it actually uses.
|
|
18
23
|
*/
|
|
19
24
|
export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {
|
|
25
|
+
private readonly oidcVerifier;
|
|
20
26
|
private readonly authConfig?;
|
|
21
|
-
|
|
27
|
+
private readonly jwtHook?;
|
|
28
|
+
private readonly oidcHook?;
|
|
29
|
+
constructor(oidcVerifier: DefaultOidcVerifier, authConfig?: AuthConfig | undefined, jwtHook?: JwtHook | undefined, oidcHook?: OidcHook | undefined);
|
|
22
30
|
filter(meta: MethodMeta, nextFilter: Service<MethodMeta, WpResponse<unknown>>): Promise<WpResponse<unknown>>;
|
|
23
|
-
private requireAuthConfig;
|
|
24
31
|
private enforceJwt;
|
|
25
32
|
private enforceOidc;
|
|
26
33
|
/** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */
|
|
@@ -8,6 +8,8 @@ const core_context_1 = require("@webpieces/core-context");
|
|
|
8
8
|
const core_util_1 = require("@webpieces/core-util");
|
|
9
9
|
const Filter_1 = require("../Filter");
|
|
10
10
|
const AuthConfig_1 = require("../AuthConfig");
|
|
11
|
+
const AuthHooks_1 = require("../AuthHooks");
|
|
12
|
+
const DefaultOidcVerifier_1 = require("../DefaultOidcVerifier");
|
|
11
13
|
const log = core_util_1.LogManager.getLogger('AuthFilter');
|
|
12
14
|
/**
|
|
13
15
|
* The ONE credential header, read straight off the inbound HttpRequest.
|
|
@@ -35,25 +37,34 @@ const PRINCIPAL_KEY = '__webpieces_principal__';
|
|
|
35
37
|
* route. It is TRANSPORT-NEUTRAL: it reads the raw credential from the {@link HttpRequest} in
|
|
36
38
|
* RequestContext (never express), so the SAME check runs over HTTP and via createApiClient.
|
|
37
39
|
*
|
|
38
|
-
* It enforces the endpoint's AuthMode
|
|
39
|
-
*
|
|
40
|
-
* -
|
|
41
|
-
*
|
|
42
|
-
* -
|
|
43
|
-
*
|
|
40
|
+
* It enforces the endpoint's AuthMode from separately-bound pieces, each OPTIONAL except the OIDC
|
|
41
|
+
* default:
|
|
42
|
+
* - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). No
|
|
43
|
+
* AuthConfig bound → no accepted secret → fail fast (401).
|
|
44
|
+
* - jwt → the bound {@link JwtHook} (`parseJwt` + `authorizeJwt`). No JwtHook bound →
|
|
45
|
+
* "not enabled" (401): JWT needs an app secret + payload shape.
|
|
46
|
+
* - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}
|
|
47
|
+
* run DIRECTLY — so a server that wires NOTHING still verifies Google OIDC.
|
|
48
|
+
* - public → BEST-EFFORT jwt parse (only if a JwtHook is bound): stamp the user's context so
|
|
49
|
+
* a logged-out page still knows who is logged in; never fails.
|
|
44
50
|
*
|
|
45
|
-
*
|
|
46
|
-
* jsonwebtoken / gcp-identity.
|
|
51
|
+
* Zero wiring = OIDC just works; an app only binds the hooks it actually uses.
|
|
47
52
|
*/
|
|
48
53
|
let AuthFilter = class AuthFilter extends Filter_1.Filter {
|
|
54
|
+
oidcVerifier;
|
|
49
55
|
authConfig;
|
|
50
|
-
|
|
56
|
+
jwtHook;
|
|
57
|
+
oidcHook;
|
|
58
|
+
constructor(oidcVerifier, authConfig, jwtHook, oidcHook) {
|
|
51
59
|
super();
|
|
60
|
+
this.oidcVerifier = oidcVerifier;
|
|
52
61
|
this.authConfig = authConfig;
|
|
62
|
+
this.jwtHook = jwtHook;
|
|
63
|
+
this.oidcHook = oidcHook;
|
|
53
64
|
}
|
|
54
65
|
// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility
|
|
55
66
|
async filter(meta, nextFilter) {
|
|
56
|
-
const mode = meta.authMeta?.mode;
|
|
67
|
+
const mode = meta.routeMeta.authMeta?.mode;
|
|
57
68
|
const authHeader = core_context_1.RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);
|
|
58
69
|
if (!mode || mode.kind === 'public') {
|
|
59
70
|
// Public: best-effort parse so a logged-out page can still know the logged-in user.
|
|
@@ -73,32 +84,34 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
|
|
|
73
84
|
}
|
|
74
85
|
return nextFilter.invoke(meta);
|
|
75
86
|
}
|
|
76
|
-
requireAuthConfig() {
|
|
77
|
-
if (!this.authConfig) {
|
|
78
|
-
throw new core_util_1.HttpUnauthorizedError('No AuthConfig bound — cannot enforce a non-public endpoint');
|
|
79
|
-
}
|
|
80
|
-
return this.authConfig;
|
|
81
|
-
}
|
|
82
87
|
enforceJwt(header, requirement) {
|
|
83
88
|
const token = this.credential(header, BEARER_SCHEME);
|
|
84
89
|
if (!token) {
|
|
85
90
|
throw new core_util_1.HttpUnauthorizedError('Authentication required');
|
|
86
91
|
}
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
if (!this.jwtHook) {
|
|
93
|
+
throw new core_util_1.HttpUnauthorizedError('User-JWT auth is not enabled on this server');
|
|
94
|
+
}
|
|
95
|
+
const values = this.jwtHook.parseJwt(token); // AUTHENTICATE — throws HttpUnauthorizedError if invalid
|
|
89
96
|
this.applyAuthValues(values);
|
|
90
|
-
|
|
97
|
+
this.jwtHook.authorizeJwt(values, requirement); // AUTHORIZE — app policy; throws HttpForbiddenError to deny
|
|
91
98
|
}
|
|
92
99
|
async enforceOidc(header, callers) {
|
|
93
100
|
const token = this.credential(header, BEARER_SCHEME);
|
|
94
101
|
if (!token) {
|
|
95
102
|
throw new core_util_1.HttpUnauthorizedError('Missing OIDC bearer token for @AuthOidc endpoint');
|
|
96
103
|
}
|
|
97
|
-
|
|
104
|
+
// App-bound OidcHook overrides the caller policy; otherwise the framework default runs directly.
|
|
105
|
+
if (this.oidcHook) {
|
|
106
|
+
await this.oidcHook.verifyOidc(token, callers);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
await this.oidcVerifier.verify(token, callers);
|
|
110
|
+
}
|
|
98
111
|
}
|
|
99
112
|
/** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */
|
|
100
113
|
enforceSharedSecret(provided, secretKey) {
|
|
101
|
-
const accepted = this.
|
|
114
|
+
const accepted = this.authConfig?.sharedSecrets[secretKey];
|
|
102
115
|
if (!accepted || !provided || !this.matchesEither(provided, accepted)) {
|
|
103
116
|
throw new core_util_1.HttpUnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');
|
|
104
117
|
}
|
|
@@ -111,12 +124,12 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
|
|
|
111
124
|
/** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */
|
|
112
125
|
bestEffortJwt(header) {
|
|
113
126
|
const token = this.credential(header, BEARER_SCHEME);
|
|
114
|
-
if (!this.
|
|
127
|
+
if (!this.jwtHook || !token) {
|
|
115
128
|
return;
|
|
116
129
|
}
|
|
117
130
|
// 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
|
|
118
131
|
try {
|
|
119
|
-
this.applyAuthValues(this.
|
|
132
|
+
this.applyAuthValues(this.jwtHook.parseJwt(token));
|
|
120
133
|
}
|
|
121
134
|
catch (err) {
|
|
122
135
|
const error = (0, core_util_1.toError)(err);
|
|
@@ -158,8 +171,16 @@ exports.AuthFilter = AuthFilter = tslib_1.__decorate([
|
|
|
158
171
|
(0, inversify_1.injectable)()
|
|
159
172
|
// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility
|
|
160
173
|
,
|
|
161
|
-
tslib_1.__param(0, (0, inversify_1.
|
|
162
|
-
tslib_1.__param(
|
|
163
|
-
tslib_1.
|
|
174
|
+
tslib_1.__param(0, (0, inversify_1.inject)(DefaultOidcVerifier_1.DefaultOidcVerifier)),
|
|
175
|
+
tslib_1.__param(1, (0, inversify_1.optional)()),
|
|
176
|
+
tslib_1.__param(1, (0, inversify_1.inject)(AuthConfig_1.AuthConfig)),
|
|
177
|
+
tslib_1.__param(2, (0, inversify_1.optional)()),
|
|
178
|
+
tslib_1.__param(2, (0, inversify_1.inject)(AuthHooks_1.JwtHook)),
|
|
179
|
+
tslib_1.__param(3, (0, inversify_1.optional)()),
|
|
180
|
+
tslib_1.__param(3, (0, inversify_1.inject)(AuthHooks_1.OidcHook)),
|
|
181
|
+
tslib_1.__metadata("design:paramtypes", [DefaultOidcVerifier_1.DefaultOidcVerifier,
|
|
182
|
+
AuthConfig_1.AuthConfig,
|
|
183
|
+
AuthHooks_1.JwtHook,
|
|
184
|
+
AuthHooks_1.OidcHook])
|
|
164
185
|
], AuthFilter);
|
|
165
186
|
//# sourceMappingURL=AuthFilter.js.map
|
|
@@ -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, injectable, optional } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport { provideFrameworkSingleton, RequestContext } from '@webpieces/core-context';\nimport { HttpUnauthorizedError, JwtRequirement, LogManager, toError } from '@webpieces/core-util';\nimport { Filter, WpResponse, Service } from '../Filter';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AuthValues, SharedSecrets } from '../AuthConfig';\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 <@AuthSharedSecret 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/** Reserved context key holding the authenticated {@link AuthValues} (stamped after a jwt parse). */\nconst PRINCIPAL_KEY = '__webpieces_principal__';\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 using the injected app-bound {@link AuthConfig}:\n * - shared-secret → constant-time compare vs the bound secret VALUE (state).\n * - jwt → `parseJwt` → stamp the user's context values + enforce @AuthJwt(...roles).\n * - oidc → `verifyOidc` (delegates to gcp-identity in the company layer).\n * - public → BEST-EFFORT jwt parse: if a token is present, stamp the user's context so a\n * logged-out page still knows who is logged in; never fails.\n *\n * The verifiers/secrets are app-provided (rebindable in tests), so http-routing needs no\n * jsonwebtoken / gcp-identity.\n */\n@provideFrameworkSingleton()\n@injectable()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\nexport class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n constructor(\n // @optional: a public-only server need not bind an AuthConfig; a non-public route then\n // fails fast in requireAuthConfig().\n @optional() @inject(AuthConfig) private readonly authConfig?: AuthConfig,\n ) {\n super();\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 mode = meta.authMeta?.mode;\n const authHeader = RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);\n\n if (!mode || mode.kind === 'public') {\n // Public: best-effort parse so a logged-out page can still know the logged-in user.\n this.bestEffortJwt(authHeader);\n return nextFilter.invoke(meta);\n }\n\n switch (mode.kind) {\n case 'jwt':\n this.enforceJwt(authHeader, mode.requirement);\n break;\n case 'oidc':\n await this.enforceOidc(authHeader, mode.callers);\n break;\n case 'shared-secret':\n this.enforceSharedSecret(this.credential(authHeader, SHARED_SECRET_SCHEME), mode.secretKey);\n break;\n }\n return nextFilter.invoke(meta);\n }\n\n private requireAuthConfig(): AuthConfig {\n if (!this.authConfig) {\n throw new HttpUnauthorizedError('No AuthConfig bound — cannot enforce a non-public endpoint');\n }\n return this.authConfig;\n }\n\n private enforceJwt(header: string | undefined, requirement: JwtRequirement): void {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new HttpUnauthorizedError('Authentication required');\n }\n const config = this.requireAuthConfig();\n const values = config.parseJwt(token); // AUTHENTICATE — throws HttpUnauthorizedError if invalid\n this.applyAuthValues(values);\n config.authorizeJwt(values, requirement); // AUTHORIZE — app policy; throws HttpForbiddenError to deny\n }\n\n private async enforceOidc(header: string | undefined, callers: string[]): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new HttpUnauthorizedError('Missing OIDC bearer token for @AuthOidc endpoint');\n }\n await this.requireAuthConfig().verifyOidc(token, callers);\n }\n\n /** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */\n private enforceSharedSecret(provided: string | undefined, secretKey: string): void {\n const accepted = this.requireAuthConfig().sharedSecrets[secretKey];\n if (!accepted || !provided || !this.matchesEither(provided, accepted)) {\n throw new HttpUnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');\n }\n }\n\n /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */\n private matchesEither(provided: string, accepted: SharedSecrets): boolean {\n return (\n (accepted.secret1 !== '' && this.constantTimeEquals(provided, accepted.secret1)) ||\n (accepted.secret2 !== '' && this.constantTimeEquals(provided, accepted.secret2))\n );\n }\n\n /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */\n private bestEffortJwt(header: string | undefined): void {\n const token = this.credential(header, BEARER_SCHEME);\n if (!this.authConfig || !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.applyAuthValues(this.authConfig.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n log.debug('Best-effort JWT parse on a public endpoint failed (treating as anonymous): ', error);\n }\n }\n\n /** Stamp the parsed user's context entries + the principal into the RequestContext. */\n private applyAuthValues(values: AuthValues): void {\n for (const entry of values.entries) {\n RequestContext.putHeader(entry.key, entry.value);\n }\n RequestContext.put(PRINCIPAL_KEY, values);\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 private 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"]}
|
|
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, injectable, optional } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport { provideFrameworkSingleton, RequestContext } from '@webpieces/core-context';\nimport { HttpUnauthorizedError, JwtRequirement, LogManager, toError } from '@webpieces/core-util';\nimport { Filter, WpResponse, Service } from '../Filter';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AuthValues, SharedSecrets } from '../AuthConfig';\nimport { JwtHook, OidcHook } from '../AuthHooks';\nimport { DefaultOidcVerifier } from '../DefaultOidcVerifier';\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 <@AuthSharedSecret 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/** Reserved context key holding the authenticated {@link AuthValues} (stamped after a jwt parse). */\nconst PRINCIPAL_KEY = '__webpieces_principal__';\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:\n * - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). No\n * AuthConfig bound → no accepted secret → fail fast (401).\n * - jwt → the bound {@link JwtHook} (`parseJwt` + `authorizeJwt`). No JwtHook bound →\n * \"not enabled\" (401): JWT needs 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.\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 *\n * Zero wiring = OIDC just works; an app only binds the hooks it actually uses.\n */\n@provideFrameworkSingleton()\n@injectable()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\nexport class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {\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 @AuthSharedSecret endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(AuthConfig) private readonly authConfig?: AuthConfig,\n // @optional: only bind a JwtHook to enable @AuthJwt endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(JwtHook) private readonly jwtHook?: JwtHook,\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(OidcHook) private readonly oidcHook?: OidcHook,\n ) {\n super();\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 mode = meta.routeMeta.authMeta?.mode;\n const authHeader = RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);\n\n if (!mode || mode.kind === 'public') {\n // Public: best-effort parse so a logged-out page can still know the logged-in user.\n this.bestEffortJwt(authHeader);\n return nextFilter.invoke(meta);\n }\n\n switch (mode.kind) {\n case 'jwt':\n this.enforceJwt(authHeader, mode.requirement);\n break;\n case 'oidc':\n await this.enforceOidc(authHeader, mode.callers);\n break;\n case 'shared-secret':\n this.enforceSharedSecret(this.credential(authHeader, SHARED_SECRET_SCHEME), mode.secretKey);\n break;\n }\n return nextFilter.invoke(meta);\n }\n\n private enforceJwt(header: string | undefined, requirement: JwtRequirement): void {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new HttpUnauthorizedError('Authentication required');\n }\n if (!this.jwtHook) {\n throw new HttpUnauthorizedError('User-JWT auth is not enabled on this server');\n }\n const values = this.jwtHook.parseJwt(token); // AUTHENTICATE — throws HttpUnauthorizedError if invalid\n this.applyAuthValues(values);\n this.jwtHook.authorizeJwt(values, requirement); // AUTHORIZE — app policy; throws HttpForbiddenError to deny\n }\n\n private async enforceOidc(header: string | undefined, callers: string[]): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new HttpUnauthorizedError('Missing OIDC bearer token for @AuthOidc endpoint');\n }\n // App-bound OidcHook overrides the caller policy; otherwise the framework default runs directly.\n if (this.oidcHook) {\n await this.oidcHook.verifyOidc(token, callers);\n } else {\n await this.oidcVerifier.verify(token, callers);\n }\n }\n\n /** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */\n private enforceSharedSecret(provided: string | undefined, secretKey: string): void {\n const accepted = this.authConfig?.sharedSecrets[secretKey];\n if (!accepted || !provided || !this.matchesEither(provided, accepted)) {\n throw new HttpUnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');\n }\n }\n\n /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */\n private matchesEither(provided: string, accepted: SharedSecrets): boolean {\n return (\n (accepted.secret1 !== '' && this.constantTimeEquals(provided, accepted.secret1)) ||\n (accepted.secret2 !== '' && this.constantTimeEquals(provided, accepted.secret2))\n );\n }\n\n /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */\n private bestEffortJwt(header: string | undefined): 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.applyAuthValues(this.jwtHook.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n log.debug('Best-effort JWT parse on a public endpoint failed (treating as anonymous): ', error);\n }\n }\n\n /** Stamp the parsed user's context entries + the principal into the RequestContext. */\n private applyAuthValues(values: AuthValues): void {\n for (const entry of values.entries) {\n RequestContext.putHeader(entry.key, entry.value);\n }\n RequestContext.put(PRINCIPAL_KEY, values);\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 private 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"]}
|
package/src/index.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export { FilterMatcher, HttpFilter } from './FilterMatcher';
|
|
|
14
14
|
export { ApiFactory } from './ApiFactory';
|
|
15
15
|
export { ApiClient, ApiClientProxy } from './ApiClient';
|
|
16
16
|
export { AuthConfig, AuthValues, SharedSecrets } from './AuthConfig';
|
|
17
|
+
export { JwtHook, OidcHook } from './AuthHooks';
|
|
18
|
+
export { DefaultOidcVerifier } from './DefaultOidcVerifier';
|
|
19
|
+
export { DefaultJwtHook } from './DefaultJwtHook';
|
|
17
20
|
export { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';
|
|
18
21
|
export { setupRuntime, RuntimeSetupOptions } from './setupRuntime';
|
|
19
22
|
export { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.WEBPIECES_CONFIG_TOKEN = exports.WebpiecesConfig = exports.RuntimeSetupOptions = exports.setupRuntime = void 0;
|
|
3
|
+
exports.OidcHook = exports.JwtHook = exports.SharedSecrets = exports.AuthValues = exports.AuthConfig = exports.ApiClient = exports.FilterMatcher = exports.RouteHandler = exports.MethodMeta = exports.FilterChain = exports.WpResponse = exports.Filter = exports.HttpRequest = exports.FilterDefinition = exports.RouteDefinition = exports.ApiRoutingFactory = exports.buildFrameworkModule = exports.provideFrameworkSingletonDefaultForApi = exports.provideFrameworkSingleton = exports.provideTransient = exports.provideSingletonDefaultForApi = exports.provideSingleton = exports.ROUTING_METADATA_KEYS = exports.SourceFile = exports.isDocumentDesign = exports.DocumentDesign = exports.METADATA_KEYS = exports.RouteMetadata = exports.AuthMeta = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.getEndpoints = exports.getApiPath = exports.Queue = exports.PubSub = exports.Rpc = exports.AuthSharedSecret = exports.AuthOidc = exports.AuthJwt = exports.Public = exports.AuthenticationConfig = exports.Authentication = exports.Endpoint = exports.ApiPath = void 0;
|
|
4
|
+
exports.WEBPIECES_CONFIG_TOKEN = exports.WebpiecesConfig = exports.RuntimeSetupOptions = exports.setupRuntime = exports.WebpiecesRouterFactory = exports.WebpiecesRouter = exports.DefaultJwtHook = exports.DefaultOidcVerifier = void 0;
|
|
5
5
|
// Re-export API decorators from core-util for convenience
|
|
6
6
|
var core_util_1 = require("@webpieces/core-util");
|
|
7
7
|
Object.defineProperty(exports, "ApiPath", { enumerable: true, get: function () { return core_util_1.ApiPath; } });
|
|
@@ -73,11 +73,22 @@ var FilterMatcher_1 = require("./FilterMatcher");
|
|
|
73
73
|
Object.defineProperty(exports, "FilterMatcher", { enumerable: true, get: function () { return FilterMatcher_1.FilterMatcher; } });
|
|
74
74
|
var ApiClient_1 = require("./ApiClient");
|
|
75
75
|
Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return ApiClient_1.ApiClient; } });
|
|
76
|
-
// Auth: the app-provided, container-bound
|
|
76
|
+
// Auth: the app-provided, container-bound pieces the framework AuthFilter injects.
|
|
77
|
+
// - AuthConfig: shared-secret STATE (@AuthSharedSecret values).
|
|
78
|
+
// - JwtHook / OidcHook: OPTIONAL verification mechanisms (bind only what you use).
|
|
79
|
+
// - DefaultOidcVerifier: the built-in Google OIDC verifier used when no OidcHook is bound.
|
|
77
80
|
var AuthConfig_1 = require("./AuthConfig");
|
|
78
81
|
Object.defineProperty(exports, "AuthConfig", { enumerable: true, get: function () { return AuthConfig_1.AuthConfig; } });
|
|
79
82
|
Object.defineProperty(exports, "AuthValues", { enumerable: true, get: function () { return AuthConfig_1.AuthValues; } });
|
|
80
83
|
Object.defineProperty(exports, "SharedSecrets", { enumerable: true, get: function () { return AuthConfig_1.SharedSecrets; } });
|
|
84
|
+
var AuthHooks_1 = require("./AuthHooks");
|
|
85
|
+
Object.defineProperty(exports, "JwtHook", { enumerable: true, get: function () { return AuthHooks_1.JwtHook; } });
|
|
86
|
+
Object.defineProperty(exports, "OidcHook", { enumerable: true, get: function () { return AuthHooks_1.OidcHook; } });
|
|
87
|
+
var DefaultOidcVerifier_1 = require("./DefaultOidcVerifier");
|
|
88
|
+
Object.defineProperty(exports, "DefaultOidcVerifier", { enumerable: true, get: function () { return DefaultOidcVerifier_1.DefaultOidcVerifier; } });
|
|
89
|
+
// DefaultJwtHook: batteries-included HS256 JwtHook — `new DefaultJwtHook(secret)` and go.
|
|
90
|
+
var DefaultJwtHook_1 = require("./DefaultJwtHook");
|
|
91
|
+
Object.defineProperty(exports, "DefaultJwtHook", { enumerable: true, get: function () { return DefaultJwtHook_1.DefaultJwtHook; } });
|
|
81
92
|
// Above-boundary context setup shared by every transport adapter.
|
|
82
93
|
// Node-only router (the express-free heart: container + filter chain + in-process client)
|
|
83
94
|
var WebpiecesRouter_1 = require("./WebpiecesRouter");
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/index.ts"],"names":[],"mappings":";;;;AAAA,0DAA0D;AAC1D,kDA8B8B;AA7B1B,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,2GAAA,cAAc,OAAA;AACd,iHAAA,oBAAoB,OAAA;AACpB,mGAAA,MAAM,OAAA;AACN,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,6GAAA,gBAAgB,OAAA;AAChB,gGAAA,GAAG,OAAA;AACH,mGAAA,MAAM,OAAA;AACN,kGAAA,KAAK,OAAA;AACL,uGAAA,UAAU,OAAA;AACV,yGAAA,YAAY,OAAA;AACZ,sGAAA,SAAS,OAAA;AACT,wGAAA,WAAW,OAAA;AACX,wGAAA,WAAW,OAAA;AACX,2HAAA,8BAA8B,OAAA;AAC9B,uGAAA,UAAU,OAAA;AACV,0GAAA,aAAa,OAAA;AACb,oHAAA,uBAAuB,OAAA;AACvB,yGAAA,YAAY,OAAA;AACZ,qGAAA,QAAQ,OAAA;AACR,0GAAA,aAAa,OAAA;AACb,0GAAA,aAAa,OAAA;AAEb,2EAA2E;AAC3E,oCAAoC;AACpC,2GAAA,cAAc,OAAA;AACd,6GAAA,gBAAgB,OAAA;AAIpB,+CAA+C;AAC/C,2CAGsB;AAFlB,wGAAA,UAAU,OAAA;AACV,mHAAA,qBAAqB,OAAA;AAGzB,iFAAiF;AACjF,wDAA4G;AAAnG,gHAAA,gBAAgB,OAAA;AAAE,6HAAA,6BAA6B,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAC1E,gGAAgG;AAChG,wDAIiC;AAH7B,yHAAA,yBAAyB,OAAA;AACzB,sIAAA,sCAAsC,OAAA;AACtC,oHAAA,oBAAoB,OAAA;AAGxB,yDAAmE;AAA1D,sHAAA,iBAAiB,OAAA;AAE1B,qBAAqB;AACrB,2CAKsB;AAFlB,6GAAA,eAAe,OAAA;AACf,8GAAA,gBAAgB,OAAA;AAGpB,sFAAsF;AACtF,+FAA+F;AAC/F,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,qFAAqF;AACrF,mCAAuD;AAA9C,gGAAA,MAAM,OAAA;AAAE,oGAAA,UAAU,OAAA;AAC3B,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,oFAAoF;AACpF,sFAAsF;AAEtF,kBAAkB;AAClB,iDAA4D;AAAnD,8GAAA,aAAa,OAAA;AAItB,yCAAwD;AAA/C,sGAAA,SAAS,OAAA;AAElB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/index.ts"],"names":[],"mappings":";;;;AAAA,0DAA0D;AAC1D,kDA8B8B;AA7B1B,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,2GAAA,cAAc,OAAA;AACd,iHAAA,oBAAoB,OAAA;AACpB,mGAAA,MAAM,OAAA;AACN,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,6GAAA,gBAAgB,OAAA;AAChB,gGAAA,GAAG,OAAA;AACH,mGAAA,MAAM,OAAA;AACN,kGAAA,KAAK,OAAA;AACL,uGAAA,UAAU,OAAA;AACV,yGAAA,YAAY,OAAA;AACZ,sGAAA,SAAS,OAAA;AACT,wGAAA,WAAW,OAAA;AACX,wGAAA,WAAW,OAAA;AACX,2HAAA,8BAA8B,OAAA;AAC9B,uGAAA,UAAU,OAAA;AACV,0GAAA,aAAa,OAAA;AACb,oHAAA,uBAAuB,OAAA;AACvB,yGAAA,YAAY,OAAA;AACZ,qGAAA,QAAQ,OAAA;AACR,0GAAA,aAAa,OAAA;AACb,0GAAA,aAAa,OAAA;AAEb,2EAA2E;AAC3E,oCAAoC;AACpC,2GAAA,cAAc,OAAA;AACd,6GAAA,gBAAgB,OAAA;AAIpB,+CAA+C;AAC/C,2CAGsB;AAFlB,wGAAA,UAAU,OAAA;AACV,mHAAA,qBAAqB,OAAA;AAGzB,iFAAiF;AACjF,wDAA4G;AAAnG,gHAAA,gBAAgB,OAAA;AAAE,6HAAA,6BAA6B,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAC1E,gGAAgG;AAChG,wDAIiC;AAH7B,yHAAA,yBAAyB,OAAA;AACzB,sIAAA,sCAAsC,OAAA;AACtC,oHAAA,oBAAoB,OAAA;AAGxB,yDAAmE;AAA1D,sHAAA,iBAAiB,OAAA;AAE1B,qBAAqB;AACrB,2CAKsB;AAFlB,6GAAA,eAAe,OAAA;AACf,8GAAA,gBAAgB,OAAA;AAGpB,sFAAsF;AACtF,+FAA+F;AAC/F,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,qFAAqF;AACrF,mCAAuD;AAA9C,gGAAA,MAAM,OAAA;AAAE,oGAAA,UAAU,OAAA;AAC3B,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,oFAAoF;AACpF,sFAAsF;AAEtF,kBAAkB;AAClB,iDAA4D;AAAnD,8GAAA,aAAa,OAAA;AAItB,yCAAwD;AAA/C,sGAAA,SAAS,OAAA;AAElB,mFAAmF;AACnF,iEAAiE;AACjE,oFAAoF;AACpF,4FAA4F;AAC5F,2CAAqE;AAA5D,wGAAA,UAAU,OAAA;AAAE,wGAAA,UAAU,OAAA;AAAE,2GAAA,aAAa,OAAA;AAC9C,yCAAgD;AAAvC,oGAAA,OAAO,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAC1B,6DAA4D;AAAnD,0HAAA,mBAAmB,OAAA;AAC5B,0FAA0F;AAC1F,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAEvB,kEAAkE;AAElE,0FAA0F;AAC1F,qDAAoG;AAA3F,kHAAA,eAAe,OAAA;AAAE,yHAAA,sBAAsB,OAAA;AAEhD,8FAA8F;AAC9F,kGAAkG;AAClG,+CAAmE;AAA1D,4GAAA,YAAY,OAAA;AAAE,mHAAA,mBAAmB,OAAA;AAE1C,uBAAuB;AACvB,qDAA4E;AAAnE,kHAAA,eAAe,OAAA;AAAE,yHAAA,sBAAsB,OAAA","sourcesContent":["// Re-export API decorators from core-util for convenience\nexport {\n ApiPath,\n Endpoint,\n Authentication,\n AuthenticationConfig,\n Public,\n AuthJwt,\n AuthOidc,\n AuthSharedSecret,\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n ValidateImplementation,\n // @DocumentDesign moved to core-util (design-root marker, browser + Node);\n // re-exported here for back-compat.\n DocumentDesign,\n isDocumentDesign,\n} from '@webpieces/core-util';\nexport type { AuthMode, ApiKind } from '@webpieces/core-util';\n\n// Server-side routing decorators and utilities\nexport {\n SourceFile,\n ROUTING_METADATA_KEYS,\n} from './decorators';\n\n// DI provider decorators moved to core-context; re-exported here for back-compat\nexport { provideSingleton, provideSingletonDefaultForApi, provideTransient } from '@webpieces/core-context';\n// Framework-only DI registry (packages/** framework classes use these; see frameworkProvide.ts)\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonDefaultForApi,\n buildFrameworkModule,\n} from '@webpieces/core-context';\n\nexport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\n\n// Core routing types\nexport {\n Routes,\n RouteBuilder,\n RouteDefinition,\n FilterDefinition,\n} from './WebAppMeta';\n\n// The transport-neutral request type (defined in core-context; this is http-routing's\n// public request — a transport adapter builds one and the chain reads it from RequestContext).\nexport { HttpRequest } from '@webpieces/core-context';\n\n// Filter-chain primitives (absorbed from the former @webpieces/http-filters package)\nexport { Filter, WpResponse, Service } from './Filter';\nexport { FilterChain } from './FilterChain';\nexport { MethodMeta } from './MethodMeta';\nexport { RouteHandler } from './RouteHandler';\n\n// RouteBuilderImpl (the route table + chain composer) is now INTERNAL — it is never\n// handed to upper layers. The express layer consumes ApiFactory.apiClients() instead.\n\n// Filter matching\nexport { FilterMatcher, HttpFilter } from './FilterMatcher';\n\n// The public API-surface abstraction: declare routes/filters, get them back as ApiClient[].\nexport { ApiFactory } from './ApiFactory';\nexport { ApiClient, ApiClientProxy } from './ApiClient';\n\n// Auth: the app-provided, container-bound pieces the framework AuthFilter injects.\n// - AuthConfig: shared-secret STATE (@AuthSharedSecret values).\n// - JwtHook / OidcHook: OPTIONAL verification mechanisms (bind only what you use).\n// - DefaultOidcVerifier: the built-in Google OIDC verifier used when no OidcHook is bound.\nexport { AuthConfig, AuthValues, SharedSecrets } from './AuthConfig';\nexport { JwtHook, OidcHook } from './AuthHooks';\nexport { DefaultOidcVerifier } from './DefaultOidcVerifier';\n// DefaultJwtHook: batteries-included HS256 JwtHook — `new DefaultJwtHook(secret)` and go.\nexport { DefaultJwtHook } from './DefaultJwtHook';\n\n// Above-boundary context setup shared by every transport adapter.\n\n// Node-only router (the express-free heart: container + filter chain + in-process client)\nexport { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';\n\n// The ONE transport-free startup sequence (headers → logging → router → routes) → ApiFactory.\n// Reusable by any company/app and any framework adapter; a company wraps it with its own headers.\nexport { setupRuntime, RuntimeSetupOptions } from './setupRuntime';\n\n// Server configuration\nexport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\n"]}
|