@webpieces/http-routing 0.3.308 → 0.3.309

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/http-routing",
3
- "version": "0.3.308",
3
+ "version": "0.3.309",
4
4
  "description": "Decorator-based routing with auto-wiring for WebPieces",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@inversifyjs/binding-decorators": "1.1.5",
25
- "@webpieces/core-context": "0.3.308",
26
- "@webpieces/core-util": "0.3.308",
25
+ "@webpieces/core-context": "0.3.309",
26
+ "@webpieces/core-util": "0.3.309",
27
27
  "inversify": "7.10.4",
28
28
  "minimatch": "10.0.1"
29
29
  }
@@ -1,4 +1,4 @@
1
- import { ContextKey } from '@webpieces/core-util';
1
+ import { ContextKey, JwtRequirement } from '@webpieces/core-util';
2
2
  /**
3
3
  * ContextValue - one (ContextKey, value) pair the JWT parse plugin wants stamped into the
4
4
  * RequestContext (e.g. USER_ID, ORG_ID). Data-only structure (a class, per the guidelines).
@@ -8,6 +8,21 @@ export declare class ContextValue {
8
8
  readonly value: unknown;
9
9
  constructor(key: ContextKey, value: unknown);
10
10
  }
11
+ /**
12
+ * SharedSecrets - the accepted values for ONE `@AuthSharedSecret(name)`. BOTH secret1 AND secret2
13
+ * are accepted — this is what makes zero-downtime ROTATION possible:
14
+ *
15
+ * to rotate: shift secret2 → secret1, and put the NEW secret in secret2. Callers cut over from
16
+ * the old value to the new during the window; once every caller sends the new one, the stale
17
+ * value falls out on the next shift. At all times EITHER key works, so no request is dropped.
18
+ *
19
+ * Data-only structure (a class, per the guidelines). Leave secret2 empty for a single secret.
20
+ */
21
+ export declare class SharedSecrets {
22
+ readonly secret1: string;
23
+ readonly secret2: string;
24
+ constructor(secret1: string, secret2: string);
25
+ }
11
26
  /**
12
27
  * AuthValues - what {@link AuthConfig.parseJwt} returns: the authenticated user's id + roles (used
13
28
  * by the framework to stamp a principal and enforce @AuthJwt(...roles)) plus any extra context
@@ -18,7 +33,8 @@ export declare class AuthValues {
18
33
  readonly userId: string;
19
34
  readonly roles: string[];
20
35
  readonly entries: ContextValue[];
21
- constructor(userId: string, roles?: string[], entries?: ContextValue[]);
36
+ readonly claims: Record<string, unknown>;
37
+ constructor(userId: string, roles?: string[], entries?: ContextValue[], claims?: Record<string, unknown>);
22
38
  }
23
39
  /**
24
40
  * AuthConfig - the ONE app-provided auth binding the framework {@link AuthFilter} injects to enforce
@@ -39,10 +55,22 @@ export declare class AuthValues {
39
55
  * (or no value/plugin for its mode) fails fast.
40
56
  */
41
57
  export declare abstract class AuthConfig {
42
- /** Expected shared-secret values keyed by the `@AuthSharedSecret(name)` name. STATE. */
43
- abstract readonly sharedSecrets: Record<string, string>;
44
- /** Parse a user JWT (kind:'jwt'); return the auth values or throw HttpUnauthorizedError. */
58
+ /**
59
+ * Accepted shared-secret values keyed by the `@AuthSharedSecret(name)` name. STATE. Each entry
60
+ * is a {@link SharedSecrets} (secret1 + secret2, either accepted) so secrets can be ROTATED
61
+ * with zero dropped requests.
62
+ */
63
+ abstract readonly sharedSecrets: Record<string, SharedSecrets>;
64
+ /** Parse a user JWT (kind:'jwt') — AUTHENTICATION only. Return who the user is, or throw. */
45
65
  abstract parseJwt(token: string): AuthValues;
46
66
  /** Verify a Google OIDC token from an allowed caller (kind:'oidc'); throw on failure. */
47
67
  abstract verifyOidc(token: string, callers: string[]): Promise<void>;
68
+ /**
69
+ * AUTHORIZATION: check the authenticated user against the endpoint's {@link JwtRequirement}.
70
+ * DEFAULT enforces `roles` (any-of; empty = any authenticated user). OVERRIDE to enforce
71
+ * app-defined requirements carried by `@Auth({...})` — e.g.
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: AuthValues, requirement: JwtRequirement): void;
48
76
  }
package/src/AuthConfig.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AuthConfig = exports.AuthValues = exports.ContextValue = void 0;
3
+ exports.AuthConfig = exports.AuthValues = exports.SharedSecrets = exports.ContextValue = void 0;
4
+ const core_util_1 = require("@webpieces/core-util");
4
5
  /**
5
6
  * ContextValue - one (ContextKey, value) pair the JWT parse plugin wants stamped into the
6
7
  * RequestContext (e.g. USER_ID, ORG_ID). Data-only structure (a class, per the guidelines).
@@ -16,6 +17,25 @@ class ContextValue {
16
17
  }
17
18
  }
18
19
  exports.ContextValue = ContextValue;
20
+ /**
21
+ * SharedSecrets - the accepted values for ONE `@AuthSharedSecret(name)`. BOTH secret1 AND secret2
22
+ * are accepted — this is what makes zero-downtime ROTATION possible:
23
+ *
24
+ * to rotate: shift secret2 → secret1, and put the NEW secret in secret2. Callers cut over from
25
+ * the old value to the new during the window; once every caller sends the new one, the stale
26
+ * value falls out on the next shift. At all times EITHER key works, so no request is dropped.
27
+ *
28
+ * Data-only structure (a class, per the guidelines). Leave secret2 empty for a single secret.
29
+ */
30
+ class SharedSecrets {
31
+ secret1;
32
+ secret2;
33
+ constructor(secret1, secret2) {
34
+ this.secret1 = secret1;
35
+ this.secret2 = secret2;
36
+ }
37
+ }
38
+ exports.SharedSecrets = SharedSecrets;
19
39
  /**
20
40
  * AuthValues - what {@link AuthConfig.parseJwt} returns: the authenticated user's id + roles (used
21
41
  * by the framework to stamp a principal and enforce @AuthJwt(...roles)) plus any extra context
@@ -26,10 +46,14 @@ class AuthValues {
26
46
  userId;
27
47
  roles;
28
48
  entries;
29
- constructor(userId, roles = [], entries = []) {
49
+ claims;
50
+ constructor(userId, roles = [], entries = [],
51
+ // webpieces-disable no-any-unknown -- raw JWT claims for app-defined authorization (inOrg, tenant, ...)
52
+ claims = {}) {
30
53
  this.userId = userId;
31
54
  this.roles = roles;
32
55
  this.entries = entries;
56
+ this.claims = claims;
33
57
  }
34
58
  }
35
59
  exports.AuthValues = AuthValues;
@@ -52,6 +76,19 @@ exports.AuthValues = AuthValues;
52
76
  * (or no value/plugin for its mode) fails fast.
53
77
  */
54
78
  class AuthConfig {
79
+ /**
80
+ * AUTHORIZATION: check the authenticated user against the endpoint's {@link JwtRequirement}.
81
+ * DEFAULT enforces `roles` (any-of; empty = any authenticated user). OVERRIDE to enforce
82
+ * app-defined requirements carried by `@Auth({...})` — e.g.
83
+ * `if (requirement['inOrg'] && !values.claims['orgId']) throw new HttpForbiddenError(...)`.
84
+ * Throw HttpForbiddenError to deny; return to allow. This is the pluggable seam.
85
+ */
86
+ authorizeJwt(values, requirement) {
87
+ const roles = requirement.roles ?? [];
88
+ if (roles.length > 0 && !roles.some((role) => values.roles.includes(role))) {
89
+ throw new core_util_1.HttpForbiddenError(`Endpoint requires one of roles: ${roles.join(', ')}`);
90
+ }
91
+ }
55
92
  }
56
93
  exports.AuthConfig = AuthConfig;
57
94
  //# sourceMappingURL=AuthConfig.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthConfig.ts"],"names":[],"mappings":";;;AAEA;;;GAGG;AACH,MAAa,YAAY;IAED;IAEA;IAHpB,YACoB,GAAe;IAC/B,oFAAoF;IACpE,KAAc;QAFd,QAAG,GAAH,GAAG,CAAY;QAEf,UAAK,GAAL,KAAK,CAAS;IAC/B,CAAC;CACP;AAND,oCAMC;AAED;;;;;GAKG;AACH,MAAa,UAAU;IAEC;IACA;IACA;IAHpB,YACoB,MAAc,EACd,QAAkB,EAAE,EACpB,UAA0B,EAAE;QAF5B,WAAM,GAAN,MAAM,CAAQ;QACd,UAAK,GAAL,KAAK,CAAe;QACpB,YAAO,GAAP,OAAO,CAAqB;IAC7C,CAAC;CACP;AAND,gCAMC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAsB,UAAU;CAS/B;AATD,gCASC","sourcesContent":["import { ContextKey } from '@webpieces/core-util';\n\n/**\n * ContextValue - one (ContextKey, value) pair the JWT parse plugin wants stamped into the\n * RequestContext (e.g. USER_ID, ORG_ID). Data-only structure (a class, per the guidelines).\n */\nexport class ContextValue {\n constructor(\n public readonly key: ContextKey,\n // webpieces-disable no-any-unknown -- context values are arbitrary app-defined data\n public readonly value: unknown,\n ) {}\n}\n\n/**\n * AuthValues - what {@link AuthConfig.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: ContextValue[] = [],\n ) {}\n}\n\n/**\n * AuthConfig - the ONE app-provided auth binding the framework {@link AuthFilter} injects to enforce\n * each endpoint's AuthMode. It is a single abstract class (injected by type, per no-symbol-di-tokens)\n * bound in the APP container and rebindable in tests. Each mechanism has its RIGHT shape:\n *\n * - `sharedSecrets` — STATE: the expected secret VALUE per name (from `@AuthSharedSecret(name)`).\n * Prod fills it from env; a test binds `{ NAME: 'some-test-key' }` and can then\n * exercise the shared-secret path (and a negative test with a wrong key).\n * - `parseJwt` — PLUGIN: decode/verify a user JWT into {@link AuthValues} (userId, roles,\n * context entries). Minting a JWT is a controller concern (login), not here.\n * - `verifyOidc` — PLUGIN: verify a Google OIDC service-to-service token against the endpoint's\n * caller allow-list. Fully generic — the company base wires it to\n * @webpieces/gcp-identity once, so apps never customize OIDC.\n *\n * Keeping the plugins app-side means http-routing needs NO jsonwebtoken / gcp-identity. AuthFilter\n * injects this `@optional`: a public-only server binds none; a non-public route with no AuthConfig\n * (or no value/plugin for its mode) fails fast.\n */\nexport abstract class AuthConfig {\n /** Expected shared-secret values keyed by the `@AuthSharedSecret(name)` name. STATE. */\n abstract readonly sharedSecrets: Record<string, string>;\n\n /** Parse a user JWT (kind:'jwt'); return the auth values or throw HttpUnauthorizedError. */\n abstract parseJwt(token: string): AuthValues;\n\n /** Verify a Google OIDC token from an allowed caller (kind:'oidc'); throw on failure. */\n abstract verifyOidc(token: string, callers: string[]): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthConfig.ts"],"names":[],"mappings":";;;AAAA,oDAAsF;AAEtF;;;GAGG;AACH,MAAa,YAAY;IAED;IAEA;IAHpB,YACoB,GAAe;IAC/B,oFAAoF;IACpE,KAAc;QAFd,QAAG,GAAH,GAAG,CAAY;QAEf,UAAK,GAAL,KAAK,CAAS;IAC/B,CAAC;CACP;AAND,oCAMC;AAED;;;;;;;;;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;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAsB,UAAU;IAc5B;;;;;;OAMG;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;AA3BD,gCA2BC","sourcesContent":["import { ContextKey, JwtRequirement, HttpForbiddenError } from '@webpieces/core-util';\n\n/**\n * ContextValue - one (ContextKey, value) pair the JWT parse plugin wants stamped into the\n * RequestContext (e.g. USER_ID, ORG_ID). Data-only structure (a class, per the guidelines).\n */\nexport class ContextValue {\n constructor(\n public readonly key: ContextKey,\n // webpieces-disable no-any-unknown -- context values are arbitrary app-defined data\n public readonly value: unknown,\n ) {}\n}\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 AuthConfig.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: ContextValue[] = [],\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 ONE app-provided auth binding the framework {@link AuthFilter} injects to enforce\n * each endpoint's AuthMode. It is a single abstract class (injected by type, per no-symbol-di-tokens)\n * bound in the APP container and rebindable in tests. Each mechanism has its RIGHT shape:\n *\n * - `sharedSecrets` — STATE: the expected secret VALUE per name (from `@AuthSharedSecret(name)`).\n * Prod fills it from env; a test binds `{ NAME: 'some-test-key' }` and can then\n * exercise the shared-secret path (and a negative test with a wrong key).\n * - `parseJwt` — PLUGIN: decode/verify a user JWT into {@link AuthValues} (userId, roles,\n * context entries). Minting a JWT is a controller concern (login), not here.\n * - `verifyOidc` — PLUGIN: verify a Google OIDC service-to-service token against the endpoint's\n * caller allow-list. Fully generic — the company base wires it to\n * @webpieces/gcp-identity once, so apps never customize OIDC.\n *\n * Keeping the plugins app-side means http-routing needs NO jsonwebtoken / gcp-identity. AuthFilter\n * injects this `@optional`: a public-only server binds none; a non-public route with no AuthConfig\n * (or no value/plugin for its mode) fails fast.\n */\nexport abstract class AuthConfig {\n /**\n * Accepted shared-secret values keyed by the `@AuthSharedSecret(name)` name. STATE. Each entry\n * is a {@link SharedSecrets} (secret1 + secret2, either accepted) so secrets can be ROTATED\n * with zero dropped requests.\n */\n abstract readonly sharedSecrets: Record<string, SharedSecrets>;\n\n /** Parse a user JWT (kind:'jwt') AUTHENTICATION only. Return who the user is, or throw. */\n abstract parseJwt(token: string): AuthValues;\n\n /** Verify a Google OIDC token from an allowed caller (kind:'oidc'); throw on failure. */\n abstract verifyOidc(token: string, callers: string[]): Promise<void>;\n\n /**\n * AUTHORIZATION: check the authenticated user against the endpoint's {@link JwtRequirement}.\n * DEFAULT enforces `roles` (any-of; empty = any authenticated user). OVERRIDE to enforce\n * app-defined requirements carried by `@Auth({...})` — e.g.\n * `if (requirement['inOrg'] && !values.claims['orgId']) throw new HttpForbiddenError(...)`.\n * Throw HttpForbiddenError to deny; return to allow. This is the pluggable seam.\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"]}
@@ -24,6 +24,8 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
24
24
  private enforceJwt;
25
25
  private enforceOidc;
26
26
  private enforceSharedSecret;
27
+ /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */
28
+ private matchesEither;
27
29
  /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */
28
30
  private bestEffortJwt;
29
31
  /** Stamp the parsed user's context entries + the principal into the RequestContext. */
@@ -43,13 +43,13 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
43
43
  }
44
44
  switch (mode.kind) {
45
45
  case 'jwt':
46
- this.enforceJwt(authHeader, mode.roles);
46
+ this.enforceJwt(authHeader, mode.requirement);
47
47
  break;
48
48
  case 'oidc':
49
49
  await this.enforceOidc(authHeader, mode.callers);
50
50
  break;
51
51
  case 'shared-secret':
52
- this.enforceSharedSecret(core_context_1.RequestContext.getRequest()?.getHeader(core_util_1.WebpiecesCoreHeaders.SHARED_SECRET), mode.secretEnv);
52
+ this.enforceSharedSecret(core_context_1.RequestContext.getRequest()?.getHeader(core_util_1.WebpiecesCoreHeaders.SHARED_SECRET), mode.secretKey);
53
53
  break;
54
54
  }
55
55
  return nextFilter.invoke(meta);
@@ -60,17 +60,15 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
60
60
  }
61
61
  return this.authConfig;
62
62
  }
63
- enforceJwt(header, roles) {
63
+ enforceJwt(header, requirement) {
64
64
  const token = this.stripBearer(header);
65
65
  if (!token) {
66
66
  throw new core_util_1.HttpUnauthorizedError('Authentication required');
67
67
  }
68
- const values = this.requireAuthConfig().parseJwt(token); // throws HttpUnauthorizedError if invalid
68
+ const config = this.requireAuthConfig();
69
+ const values = config.parseJwt(token); // AUTHENTICATE — throws HttpUnauthorizedError if invalid
69
70
  this.applyAuthValues(values);
70
- // @AuthJwt(...roles): empty = any authenticated user; non-empty = must hold at least one.
71
- if (roles.length > 0 && !roles.some((role) => values.roles.includes(role))) {
72
- throw new core_util_1.HttpForbiddenError(`Endpoint requires one of roles: ${roles.join(', ')}`);
73
- }
71
+ config.authorizeJwt(values, requirement); // AUTHORIZE app policy; throws HttpForbiddenError to deny
74
72
  }
75
73
  async enforceOidc(header, callers) {
76
74
  const token = this.stripBearer(header);
@@ -79,12 +77,17 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
79
77
  }
80
78
  await this.requireAuthConfig().verifyOidc(token, callers);
81
79
  }
82
- enforceSharedSecret(provided, secretEnv) {
83
- const expected = this.requireAuthConfig().sharedSecrets[secretEnv];
84
- if (!expected || !provided || !this.constantTimeEquals(provided, expected)) {
80
+ enforceSharedSecret(provided, secretKey) {
81
+ const accepted = this.requireAuthConfig().sharedSecrets[secretKey];
82
+ if (!accepted || !provided || !this.matchesEither(provided, accepted)) {
85
83
  throw new core_util_1.HttpUnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');
86
84
  }
87
85
  }
86
+ /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */
87
+ matchesEither(provided, accepted) {
88
+ return ((accepted.secret1 !== '' && this.constantTimeEquals(provided, accepted.secret1)) ||
89
+ (accepted.secret2 !== '' && this.constantTimeEquals(provided, accepted.secret2)));
90
+ }
88
91
  /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */
89
92
  bestEffortJwt(header) {
90
93
  const token = this.stripBearer(header);
@@ -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 { WebpiecesCoreHeaders, HttpUnauthorizedError, HttpForbiddenError, LogManager, toError } from '@webpieces/core-util';\nimport { Filter, WpResponse, Service } from '../Filter';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AuthValues } from '../AuthConfig';\n\nconst log = LogManager.getLogger('AuthFilter');\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(WebpiecesCoreHeaders.AUTHORIZATION);\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.roles);\n break;\n case 'oidc':\n await this.enforceOidc(authHeader, mode.callers);\n break;\n case 'shared-secret':\n this.enforceSharedSecret(\n RequestContext.getRequest()?.getHeader(WebpiecesCoreHeaders.SHARED_SECRET),\n mode.secretEnv,\n );\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, roles: string[]): void {\n const token = this.stripBearer(header);\n if (!token) {\n throw new HttpUnauthorizedError('Authentication required');\n }\n const values = this.requireAuthConfig().parseJwt(token); // throws HttpUnauthorizedError if invalid\n this.applyAuthValues(values);\n // @AuthJwt(...roles): empty = any authenticated user; non-empty = must hold at least one.\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 private async enforceOidc(header: string | undefined, callers: string[]): Promise<void> {\n const token = this.stripBearer(header);\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 private enforceSharedSecret(provided: string | undefined, secretEnv: string): void {\n const expected = this.requireAuthConfig().sharedSecrets[secretEnv];\n if (!expected || !provided || !this.constantTimeEquals(provided, expected)) {\n throw new HttpUnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');\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.stripBearer(header);\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 private stripBearer(header: string | undefined): string | undefined {\n if (!header) {\n return undefined;\n }\n const prefix = 'Bearer ';\n return header.startsWith(prefix) ? header.substring(prefix.length) : header;\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 { WebpiecesCoreHeaders, 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/** 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(WebpiecesCoreHeaders.AUTHORIZATION);\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(\n RequestContext.getRequest()?.getHeader(WebpiecesCoreHeaders.SHARED_SECRET),\n mode.secretKey,\n );\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.stripBearer(header);\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.stripBearer(header);\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 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.stripBearer(header);\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 private stripBearer(header: string | undefined): string | undefined {\n if (!header) {\n return undefined;\n }\n const prefix = 'Bearer ';\n return header.startsWith(prefix) ? header.substring(prefix.length) : header;\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
@@ -13,7 +13,7 @@ export { RouteHandler } from './RouteHandler';
13
13
  export { FilterMatcher, HttpFilter } from './FilterMatcher';
14
14
  export { ApiFactory } from './ApiFactory';
15
15
  export { ApiClient, ApiClientProxy } from './ApiClient';
16
- export { AuthConfig, AuthValues, ContextValue } from './AuthConfig';
16
+ export { AuthConfig, AuthValues, ContextValue, SharedSecrets } from './AuthConfig';
17
17
  export { fillContext } from './fillContext';
18
18
  export { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';
19
19
  export { setupRuntime, RuntimeSetupOptions } from './setupRuntime';
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WebpiecesRouter = exports.fillContext = exports.ContextValue = 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.provideFrameworkSingletonAs = exports.provideFrameworkSingleton = exports.provideTransient = exports.provideSingletonAs = 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.RequestContextReader = exports.RuntimeSetupOptions = exports.setupRuntime = exports.WebpiecesRouterFactory = void 0;
3
+ exports.fillContext = exports.SharedSecrets = exports.ContextValue = 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.provideFrameworkSingletonAs = exports.provideFrameworkSingleton = exports.provideTransient = exports.provideSingletonAs = 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.RequestContextReader = exports.RuntimeSetupOptions = exports.setupRuntime = exports.WebpiecesRouterFactory = exports.WebpiecesRouter = 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; } });
@@ -78,6 +78,7 @@ var AuthConfig_1 = require("./AuthConfig");
78
78
  Object.defineProperty(exports, "AuthConfig", { enumerable: true, get: function () { return AuthConfig_1.AuthConfig; } });
79
79
  Object.defineProperty(exports, "AuthValues", { enumerable: true, get: function () { return AuthConfig_1.AuthValues; } });
80
80
  Object.defineProperty(exports, "ContextValue", { enumerable: true, get: function () { return AuthConfig_1.ContextValue; } });
81
+ Object.defineProperty(exports, "SharedSecrets", { enumerable: true, get: function () { return AuthConfig_1.SharedSecrets; } });
81
82
  // Above-boundary context setup shared by every transport adapter.
82
83
  var fillContext_1 = require("./fillContext");
83
84
  Object.defineProperty(exports, "fillContext", { enumerable: true, get: function () { return fillContext_1.fillContext; } });
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,wDAAiG;AAAxF,gHAAA,gBAAgB,OAAA;AAAE,kHAAA,kBAAkB,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAC/D,gGAAgG;AAChG,wDAIiC;AAH7B,yHAAA,yBAAyB,OAAA;AACzB,2HAAA,2BAA2B,OAAA;AAC3B,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,uFAAuF;AACvF,2CAAoE;AAA3D,wGAAA,UAAU,OAAA;AAAE,wGAAA,UAAU,OAAA;AAAE,0GAAA,YAAY,OAAA;AAE7C,kEAAkE;AAClE,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AAEpB,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,oFAAoF;AACpF,wDAA+D;AAAtD,oHAAA,oBAAoB,OAAA;AAE7B,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, provideSingletonAs, provideTransient } from '@webpieces/core-context';\n// Framework-only DI registry (packages/** framework classes use these; see frameworkProvide.ts)\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonAs,\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 AuthConfig the framework AuthFilter injects.\nexport { AuthConfig, AuthValues, ContextValue } from './AuthConfig';\n\n// Above-boundary context setup shared by every transport adapter.\nexport { fillContext } from './fillContext';\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// Context readers (Node.js only) moved to core-context; re-exported for back-compat\nexport { RequestContextReader } from '@webpieces/core-context';\n\n// Server configuration\nexport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\n"]}
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,wDAAiG;AAAxF,gHAAA,gBAAgB,OAAA;AAAE,kHAAA,kBAAkB,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAC/D,gGAAgG;AAChG,wDAIiC;AAH7B,yHAAA,yBAAyB,OAAA;AACzB,2HAAA,2BAA2B,OAAA;AAC3B,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,uFAAuF;AACvF,2CAAmF;AAA1E,wGAAA,UAAU,OAAA;AAAE,wGAAA,UAAU,OAAA;AAAE,0GAAA,YAAY,OAAA;AAAE,2GAAA,aAAa,OAAA;AAE5D,kEAAkE;AAClE,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AAEpB,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,oFAAoF;AACpF,wDAA+D;AAAtD,oHAAA,oBAAoB,OAAA;AAE7B,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, provideSingletonAs, provideTransient } from '@webpieces/core-context';\n// Framework-only DI registry (packages/** framework classes use these; see frameworkProvide.ts)\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonAs,\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 AuthConfig the framework AuthFilter injects.\nexport { AuthConfig, AuthValues, ContextValue, SharedSecrets } from './AuthConfig';\n\n// Above-boundary context setup shared by every transport adapter.\nexport { fillContext } from './fillContext';\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// Context readers (Node.js only) moved to core-context; re-exported for back-compat\nexport { RequestContextReader } from '@webpieces/core-context';\n\n// Server configuration\nexport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\n"]}