@webpieces/http-routing 0.4.862 → 0.4.864

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.
Files changed (52) hide show
  1. package/README.md +3 -1
  2. package/package.json +4 -4
  3. package/src/ApiClientFactory.js +1 -1
  4. package/src/ApiClientFactory.js.map +1 -1
  5. package/src/ApiFactory.d.ts +3 -0
  6. package/src/ApiFactory.js.map +1 -1
  7. package/src/ApiRoutingFactory.js +3 -3
  8. package/src/ApiRoutingFactory.js.map +1 -1
  9. package/src/AuthConfig.d.ts +15 -8
  10. package/src/AuthConfig.js +20 -8
  11. package/src/AuthConfig.js.map +1 -1
  12. package/src/AuthHooks.d.ts +13 -61
  13. package/src/AuthHooks.js +7 -44
  14. package/src/AuthHooks.js.map +1 -1
  15. package/src/AuthenticatedCallerContext.d.ts +6 -0
  16. package/src/AuthenticatedCallerContext.js +44 -0
  17. package/src/AuthenticatedCallerContext.js.map +1 -1
  18. package/src/AuthorizationHook.d.ts +34 -0
  19. package/src/AuthorizationHook.js +121 -0
  20. package/src/AuthorizationHook.js.map +1 -0
  21. package/src/AuthorizedApiDocument.d.ts +18 -0
  22. package/src/AuthorizedApiDocument.js +137 -0
  23. package/src/AuthorizedApiDocument.js.map +1 -0
  24. package/src/DefaultJwtHook.d.ts +4 -4
  25. package/src/DefaultJwtHook.js +10 -7
  26. package/src/DefaultJwtHook.js.map +1 -1
  27. package/src/DefaultOidcVerifier.d.ts +4 -3
  28. package/src/DefaultOidcVerifier.js +6 -4
  29. package/src/DefaultOidcVerifier.js.map +1 -1
  30. package/src/InvocationAuthentication.d.ts +7 -7
  31. package/src/InvocationAuthentication.js +18 -17
  32. package/src/InvocationAuthentication.js.map +1 -1
  33. package/src/JwtHookCompileAssertions.d.ts +1 -1
  34. package/src/JwtHookCompileAssertions.js +2 -5
  35. package/src/JwtHookCompileAssertions.js.map +1 -1
  36. package/src/RouteBuilderImpl.js +7 -0
  37. package/src/RouteBuilderImpl.js.map +1 -1
  38. package/src/WebpiecesRouter.d.ts +2 -0
  39. package/src/WebpiecesRouter.js +6 -0
  40. package/src/WebpiecesRouter.js.map +1 -1
  41. package/src/filters/AuthFilter.d.ts +11 -22
  42. package/src/filters/AuthFilter.js +75 -80
  43. package/src/filters/AuthFilter.js.map +1 -1
  44. package/src/filters/AuthorizationFilter.d.ts +10 -0
  45. package/src/filters/AuthorizationFilter.js +34 -0
  46. package/src/filters/AuthorizationFilter.js.map +1 -0
  47. package/src/index.d.ts +5 -2
  48. package/src/index.js +25 -10
  49. package/src/index.js.map +1 -1
  50. package/src/setupRuntime.d.ts +2 -2
  51. package/src/setupRuntime.js +2 -2
  52. package/src/setupRuntime.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthorizedApiDocument.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthorizedApiDocument.ts"],"names":[],"mappings":";;;AAAA,oDAAyG;AAEzG,6EAA0E;AAK1E,uFAAuF;AACvF,MAAa,qBAAqB;IAID;IAHZ,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC1C,OAAO,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAEjG,YAA6B,MAA4B,EAAE,MAAgC;QAA9D,WAAM,GAAN,MAAM,CAAsB;QACrD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACzB,IAAI,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,KAAK,CAAC,QAAQ;gBAAE,MAAM,IAAI,kCAAsB,CAAC,gEAAgE,CAAC,CAAC;YAChJ,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YACrC,MAAM,EAAE,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;YACxD,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,MAAM,IAAI,kCAAsB,CAAC,gCAAgC,EAAE,GAAG,CAAC,CAAC;YACjG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC/B,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAA0B;QACpC,IAAI,CAAC,uDAA0B,CAAC,0BAA0B,EAAE;YAAE,MAAM,IAAI,kCAAsB,CAAC,sDAAsD,CAAC,CAAC;QACvJ,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAChC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;oBAAE,SAAS;gBAC7C,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;gBAC5C,MAAM,EAAE,GAAG,SAAS,CAAC,aAAa,CAAC,CAAC;gBACpC,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBACvE,IAAI,CAAC,KAAK,EAAE,aAAa,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC,WAAW,EAAE,KAAK,MAAM;oBAAE,MAAM,IAAI,kCAAsB,CAAC,sBAAsB,MAAM,CAAC,EAAE,CAAC,6CAA6C,CAAC,CAAC;gBACnP,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;oBAAC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;oBAAC,SAAS;gBAAC,CAAC;gBACpE,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;gBAChC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;oBAAE,KAAK,MAAM,IAAI,IAAI,KAAK;wBAAE,IAAI,OAAO,IAAI,KAAK,QAAQ;4BAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACrG,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAc,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC;QACvG,CAAC;QACD,OAAO,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC5B,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAqB,EAAE,EAAE;gBACtG,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;gBACtC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACtD,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QAC/B,OAAO,QAAQ,CAAC;IACpB,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,KAAoB;QACtC,IAAI,CAAC,KAAK,CAAC,aAAa;YAAE,MAAM,IAAI,kCAAsB,CAAC,iCAAiC,CAAC,CAAC;QAC9F,qKAAqK;QACrK,mJAAmJ;QACnJ,IAAI,CAAC;YACD,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YACjD,OAAO,IAAI,CAAC;QAChB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,KAAK,YAAY,6BAAiB;gBAAE,OAAO,KAAK,CAAC;YACrD,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;IAEO,eAAe,CAAC,QAA2B;QAC/C,IAAI,QAAQ,CAAC,YAAY,CAAC,KAAK,SAAS;YAAE,OAAO;QACjD,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;QACxD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;QAClC,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,YAAY,CAAC,CAAC;QAC3B,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAChC,OAAO,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAG,CAAC;YAC3B,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC/B,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACjB,MAAM,KAAK,GAAG,mCAAmC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC5D,IAAI,CAAC,KAAK;gBAAE,MAAM,IAAI,kCAAsB,CAAC,6EAA6E,CAAC,CAAC;YAC5H,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACjD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAC9D,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,SAAS;gBAAE,MAAM,IAAI,kCAAsB,CAAC,sBAAsB,GAAG,cAAc,CAAC,CAAC;YACzG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;QAC1C,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YACtD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACjC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACpC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;gBAC9D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,IAAI,OAAO,EAAE,CAAC;oBAAE,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5E,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM;gBAAE,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC;QAC7D,CAAC;IACL,CAAC;IAEO,UAAU,CAAC,KAAuB,EAAE,OAAiB;QACzD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO;QAChD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAAC,KAAK,MAAM,KAAK,IAAI,KAAK;gBAAE,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAAC,OAAO;QAAC,CAAC;QACjG,IAAI,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;QACnC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;YAAE,KAAK,MAAM,WAAW,IAAI,QAAQ,EAAE,CAAC;gBAC9D,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;oBAAE,OAAO,CAAC,IAAI,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC;YACnH,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC9E,CAAC;IAEO,MAAM,CAAC,KAAmC;QAC9C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,kCAAsB,CAAC,sCAAsC,CAAC,CAAC;QAC1I,OAAO,KAAK,CAAC;IACjB,CAAC;CACJ;AApGD,sDAoGC","sourcesContent":["import { ApiForbiddenError, ApiImplementationError, RouteMetadata, toError } from '@webpieces/core-util';\nimport { AuthorizationService } from './AuthorizationHook';\nimport { AuthenticatedCallerContext } from './AuthenticatedCallerContext';\n\nexport type ApiDocumentValue = string | number | boolean | null | ApiDocumentValue[] | ApiDocumentObject;\nexport type ApiDocumentObject = { [key: string]: ApiDocumentValue };\n\n/** Project a private bundled catalog afresh inside each authenticated docs request. */\nexport class AuthorizedApiDocument {\n private readonly routes = new Map<string, RouteMetadata>();\n private readonly methods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];\n\n constructor(private readonly policy: AuthorizationService, routes: readonly RouteMetadata[]) {\n for (const route of routes) {\n if (!route.authorization || !route.apiClass) throw new ApiImplementationError('Document routes require canonical receiving endpoint metadata.');\n policy.validate(route.authorization);\n const id = `${route.apiClass.name}_${route.methodName}`;\n if (this.routes.has(id)) throw new ApiImplementationError(`Duplicate document operation ${id}.`);\n this.routes.set(id, route);\n }\n }\n\n async project(catalog: ApiDocumentObject): Promise<ApiDocumentObject> {\n if (!AuthenticatedCallerContext.hasCompletedAuthentication()) throw new ApiImplementationError('Serve API documents inside completed authentication.');\n const document = structuredClone(catalog);\n const paths = this.object(document['paths']);\n const tags = new Set<string>();\n for (const [path, value] of Object.entries(paths)) {\n const item = this.object(value);\n for (const method of Object.keys(item)) {\n if (!this.methods.includes(method)) continue;\n const operation = this.object(item[method]);\n const id = operation['operationId'];\n const route = typeof id === 'string' ? this.routes.get(id) : undefined;\n if (!route?.authorization || route.path.replace(/:([A-Za-z_]\\w*)/g, '{$1}') !== path || route.httpMethod.toLowerCase() !== method) throw new ApiImplementationError(`Document operation ${String(id)} has no matching receiving endpoint policy.`);\n if (!(await this.permits(route))) { delete item[method]; continue; }\n const names = operation['tags'];\n if (Array.isArray(names)) for (const name of names) if (typeof name === 'string') tags.add(name);\n }\n if (!Object.keys(item).some((method: string) => this.methods.includes(method))) delete paths[path];\n }\n delete document['webhooks'];\n if (Array.isArray(document['tags'])) document['tags'] = document['tags'].filter((tag: ApiDocumentValue) => {\n const name = this.object(tag)['name'];\n return typeof name === 'string' && tags.has(name);\n });\n this.pruneComponents(document);\n return document;\n }\n\n private async permits(route: RouteMetadata): Promise<boolean> {\n if (!route.authorization) throw new ApiImplementationError('Missing document authorization.');\n // webpieces-disable no-unmanaged-exceptions -- document projection boundary turns only explicit policy denials into omitted operations; invariant failures propagate\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- explicit policy denials are omitted document operations; other failures propagate\n try {\n await this.policy.authorize(route.authorization);\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof ApiForbiddenError) return false;\n throw error;\n }\n }\n\n private pruneComponents(document: ApiDocumentObject): void {\n if (document['components'] === undefined) return;\n const definitions = this.object(document['components']);\n const reached = new Set<string>();\n const pending: string[] = [];\n const roots = { ...document };\n delete roots['components'];\n this.references(roots, pending);\n while (pending.length) {\n const ref = pending.pop()!;\n if (reached.has(ref)) continue;\n reached.add(ref);\n const match = /^#\\/components\\/([^/]+)\\/([^/]+)$/.exec(ref);\n if (!match) throw new ApiImplementationError('Caller documents require a bundled catalog with local component references.');\n const group = this.object(definitions[match[1]]);\n const name = match[2].replace(/~1/g, '/').replace(/~0/g, '~');\n if (group[name] === undefined) throw new ApiImplementationError(`Document reference ${ref} is missing.`);\n this.references(group[name], pending);\n }\n for (const [kind, value] of Object.entries(definitions)) {\n const group = this.object(value);\n for (const name of Object.keys(group)) {\n const encoded = name.replace(/~/g, '~0').replace(/\\//g, '~1');\n if (!reached.has(`#/components/${kind}/${encoded}`)) delete group[name];\n }\n if (!Object.keys(group).length) delete definitions[kind];\n }\n }\n\n private references(value: ApiDocumentValue, pending: string[]): void {\n if (!value || typeof value !== 'object') return;\n if (Array.isArray(value)) { for (const child of value) this.references(child, pending); return; }\n if (typeof value['$ref'] === 'string') pending.push(value['$ref']);\n const security = value['security'];\n if (Array.isArray(security)) for (const requirement of security) {\n for (const name of Object.keys(this.object(requirement))) pending.push(`#/components/securitySchemes/${name}`);\n }\n for (const child of Object.values(value)) this.references(child, pending);\n }\n\n private object(value: ApiDocumentValue | undefined): ApiDocumentObject {\n if (!value || typeof value !== 'object' || Array.isArray(value)) throw new ApiImplementationError('API document requires object values.');\n return value;\n }\n}\n"]}
@@ -12,12 +12,12 @@ export declare class DefaultJwtMintRequest {
12
12
  /**
13
13
  * DefaultJwtHook - a batteries-included {@link JwtHook} for the common case: HS256 user JWTs signed
14
14
  * with ONE shared secret. Construct it with the secret and bind it — `new DefaultJwtHook(secret)` —
15
- * and `@WpAuthJwt` endpoints work with NO custom verification code.
15
+ * and `jwt()` endpoints work with NO custom verification code.
16
16
  *
17
17
  * `parseJwt` verifies the signature + expiry (jsonwebtoken, HS256 only) and maps standard claims:
18
- * `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. `authorizeJwt`
19
- * (role enforcement) is inherited from JwtHook. For RS256 + JWKS, a provider SDK, or a non-standard
20
- * payload, write your own JwtHook subclass instead.
18
+ * `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. AuthorizationService
19
+ * separately enforces @WpAuthorization; AuthorizationHook owns CUSTOM application policy.
20
+ * For RS256 + JWKS, a provider SDK, or a non-standard payload, write your own JwtHook subclass.
21
21
  *
22
22
  * It satisfies {@link JwtHook}'s ASYNC signature with a body that awaits NOTHING, and that is the
23
23
  * point rather than an oversight: HS256 against a local secret is pure CPU. The signature is async
@@ -23,12 +23,12 @@ exports.DefaultJwtMintRequest = DefaultJwtMintRequest;
23
23
  /**
24
24
  * DefaultJwtHook - a batteries-included {@link JwtHook} for the common case: HS256 user JWTs signed
25
25
  * with ONE shared secret. Construct it with the secret and bind it — `new DefaultJwtHook(secret)` —
26
- * and `@WpAuthJwt` endpoints work with NO custom verification code.
26
+ * and `jwt()` endpoints work with NO custom verification code.
27
27
  *
28
28
  * `parseJwt` verifies the signature + expiry (jsonwebtoken, HS256 only) and maps standard claims:
29
- * `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. `authorizeJwt`
30
- * (role enforcement) is inherited from JwtHook. For RS256 + JWKS, a provider SDK, or a non-standard
31
- * payload, write your own JwtHook subclass instead.
29
+ * `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. AuthorizationService
30
+ * separately enforces @WpAuthorization; AuthorizationHook owns CUSTOM application policy.
31
+ * For RS256 + JWKS, a provider SDK, or a non-standard payload, write your own JwtHook subclass.
32
32
  *
33
33
  * It satisfies {@link JwtHook}'s ASYNC signature with a body that awaits NOTHING, and that is the
34
34
  * point rather than an oversight: HS256 against a local secret is pure CPU. The signature is async
@@ -83,10 +83,13 @@ class DefaultJwtHook extends AuthHooks_1.JwtHook {
83
83
  }
84
84
  extractRoles(payload) {
85
85
  const roles = payload['roles'];
86
- if (Array.isArray(roles)) {
87
- return roles.filter((role) => typeof role === 'string');
86
+ if (roles === undefined)
87
+ return [];
88
+ // webpieces-disable no-any-unknown -- verified external JWT claim elements require runtime validation
89
+ if (!Array.isArray(roles) || !roles.every((role) => typeof role === 'string' && role.trim().length > 0)) {
90
+ throw new core_util_1.ApiUnauthorizedError('JWT roles must be an array of nonempty strings.');
88
91
  }
89
- return [];
92
+ return roles;
90
93
  }
91
94
  }
92
95
  exports.DefaultJwtHook = DefaultJwtHook;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultJwtHook.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/DefaultJwtHook.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { sign, verify, JwtPayload } from 'jsonwebtoken';\nimport { ApiUnauthorizedError, toError } from '@webpieces/core-util';\nimport { JwtHook, MintedJwt } from './AuthHooks';\nimport { AuthenticatedCaller } from './AuthConfig';\n\n/** Mint input for the optional batteries-included HS256 authority. Custom authorities own their input type. */\nexport class DefaultJwtMintRequest {\n constructor(\n public readonly subject: string,\n public readonly roles: readonly string[],\n public readonly expiresInSeconds: number,\n public readonly claims: Readonly<JwtPayload>,\n ) {}\n}\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 `@WpAuthJwt` 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 *\n * It satisfies {@link JwtHook}'s ASYNC signature with a body that awaits NOTHING, and that is the\n * point rather than an oversight: HS256 against a local secret is pure CPU. The signature is async\n * because the hook is the APP's seam and an app's strategy reaches the network — not because this\n * implementation does. No fake await is added to justify it.\n */\nexport class DefaultJwtHook extends JwtHook<DefaultJwtMintRequest> {\n private readonly secret: string;\n\n constructor(secret: string) {\n super();\n this.secret = secret;\n }\n\n override async mint(request: DefaultJwtMintRequest): Promise<MintedJwt> {\n if (request.expiresInSeconds <= 0) {\n throw new Error('JWT lifetime must be greater than zero seconds.');\n }\n // webpieces-disable no-anonymous-object-literals -- jsonwebtoken payload is an external claim bag\n const payload = { ...request.claims, roles: request.roles };\n const token = sign(payload, this.secret, {\n algorithm: 'HS256',\n subject: request.subject,\n expiresIn: request.expiresInSeconds,\n jwtid: randomUUID(),\n });\n return new MintedJwt(token, Math.floor(Date.now() / 1000) + request.expiresInSeconds);\n }\n\n override async parseJwt(token: string): Promise<AuthenticatedCaller> {\n const payload = this.verifyToken(token);\n const userId = payload.sub;\n if (!userId) {\n throw new ApiUnauthorizedError('JWT is missing the required \"sub\" (subject) claim');\n }\n return new AuthenticatedCaller(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 ApiUnauthorizedError('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 ApiUnauthorizedError) {\n throw error;\n }\n throw new ApiUnauthorizedError('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"]}
1
+ {"version":3,"file":"DefaultJwtHook.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/DefaultJwtHook.ts"],"names":[],"mappings":";;;AAAA,6CAAyC;AACzC,+CAAwD;AACxD,oDAAqE;AACrE,2CAAiD;AACjD,6CAAmD;AAEnD,+GAA+G;AAC/G,MAAa,qBAAqB;IAEV;IACA;IACA;IACA;IAJpB,YACoB,OAAe,EACf,KAAwB,EACxB,gBAAwB,EACxB,MAA4B;QAH5B,YAAO,GAAP,OAAO,CAAQ;QACf,UAAK,GAAL,KAAK,CAAmB;QACxB,qBAAgB,GAAhB,gBAAgB,CAAQ;QACxB,WAAM,GAAN,MAAM,CAAsB;IAC7C,CAAC;CACP;AAPD,sDAOC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAa,cAAe,SAAQ,mBAA8B;IAC7C,MAAM,CAAS;IAEhC,YAAY,MAAc;QACtB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAEQ,KAAK,CAAC,IAAI,CAAC,OAA8B;QAC9C,IAAI,OAAO,CAAC,gBAAgB,IAAI,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACvE,CAAC;QACD,kGAAkG;QAClG,MAAM,OAAO,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC5D,MAAM,KAAK,GAAG,IAAA,mBAAI,EAAC,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE;YACrC,SAAS,EAAE,OAAO;YAClB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,SAAS,EAAE,OAAO,CAAC,gBAAgB;YACnC,KAAK,EAAE,IAAA,wBAAU,GAAE;SACtB,CAAC,CAAC;QACH,OAAO,IAAI,qBAAS,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1F,CAAC;IAEQ,KAAK,CAAC,QAAQ,CAAC,KAAa;QACjC,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,gCAAoB,CAAC,mDAAmD,CAAC,CAAC;QACxF,CAAC;QACD,OAAO,IAAI,gCAAmB,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;IACpF,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,gCAAoB,CAAC,iDAAiD,CAAC,CAAC;YACtF,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,gCAAoB,EAAE,CAAC;gBACxC,MAAM,KAAK,CAAC;YAChB,CAAC;YACD,MAAM,IAAI,gCAAoB,CAAC,yBAAyB,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QAChF,CAAC;IACL,CAAC;IAEO,YAAY,CAAC,OAAmB;QACpC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,EAAE,CAAC;QACnC,sGAAsG;QACtG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAa,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;YAC/G,MAAM,IAAI,gCAAoB,CAAC,iDAAiD,CAAC,CAAC;QACtF,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;CACJ;AA3DD,wCA2DC","sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { sign, verify, JwtPayload } from 'jsonwebtoken';\nimport { ApiUnauthorizedError, toError } from '@webpieces/core-util';\nimport { JwtHook, MintedJwt } from './AuthHooks';\nimport { AuthenticatedCaller } from './AuthConfig';\n\n/** Mint input for the optional batteries-included HS256 authority. Custom authorities own their input type. */\nexport class DefaultJwtMintRequest {\n constructor(\n public readonly subject: string,\n public readonly roles: readonly string[],\n public readonly expiresInSeconds: number,\n public readonly claims: Readonly<JwtPayload>,\n ) {}\n}\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 `jwt()` 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. AuthorizationService\n * separately enforces @WpAuthorization; AuthorizationHook owns CUSTOM application policy.\n * For RS256 + JWKS, a provider SDK, or a non-standard payload, write your own JwtHook subclass.\n *\n * It satisfies {@link JwtHook}'s ASYNC signature with a body that awaits NOTHING, and that is the\n * point rather than an oversight: HS256 against a local secret is pure CPU. The signature is async\n * because the hook is the APP's seam and an app's strategy reaches the network — not because this\n * implementation does. No fake await is added to justify it.\n */\nexport class DefaultJwtHook extends JwtHook<DefaultJwtMintRequest> {\n private readonly secret: string;\n\n constructor(secret: string) {\n super();\n this.secret = secret;\n }\n\n override async mint(request: DefaultJwtMintRequest): Promise<MintedJwt> {\n if (request.expiresInSeconds <= 0) {\n throw new Error('JWT lifetime must be greater than zero seconds.');\n }\n // webpieces-disable no-anonymous-object-literals -- jsonwebtoken payload is an external claim bag\n const payload = { ...request.claims, roles: request.roles };\n const token = sign(payload, this.secret, {\n algorithm: 'HS256',\n subject: request.subject,\n expiresIn: request.expiresInSeconds,\n jwtid: randomUUID(),\n });\n return new MintedJwt(token, Math.floor(Date.now() / 1000) + request.expiresInSeconds);\n }\n\n override async parseJwt(token: string): Promise<AuthenticatedCaller> {\n const payload = this.verifyToken(token);\n const userId = payload.sub;\n if (!userId) {\n throw new ApiUnauthorizedError('JWT is missing the required \"sub\" (subject) claim');\n }\n return new AuthenticatedCaller(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 ApiUnauthorizedError('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 ApiUnauthorizedError) {\n throw error;\n }\n throw new ApiUnauthorizedError('JWT verification failed', undefined, error);\n }\n }\n\n private extractRoles(payload: JwtPayload): string[] {\n const roles = payload['roles'];\n if (roles === undefined) return [];\n // webpieces-disable no-any-unknown -- verified external JWT claim elements require runtime validation\n if (!Array.isArray(roles) || !roles.every((role: unknown) => typeof role === 'string' && role.trim().length > 0)) {\n throw new ApiUnauthorizedError('JWT roles must be an array of nonempty strings.');\n }\n return roles;\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { AuthenticatedMachineIdentity } from './AuthConfig';
1
2
  import { GcpOidc } from '@webpieces/gcp-identity';
2
3
  /**
3
4
  * DefaultOidcVerifier - the framework's built-in Google OIDC verifier, injected into
@@ -5,10 +6,10 @@ import { GcpOidc } from '@webpieces/gcp-identity';
5
6
  * OIDC "just work" with ZERO wiring: http-routing depends on @webpieces/gcp-identity ON PURPOSE so a
6
7
  * server that binds nothing still verifies service-to-service OIDC.
7
8
  *
8
- * `verify` honors the endpoint's `@WpAuthOidc(...callers)` contract exactly: EMPTY (a bare `@WpAuthOidc()`)
9
+ * `verify` honors the endpoint's `oidc(...callers)` contract exactly: EMPTY (a bare `@WpAuth([oidc()])`)
9
10
  * = TRUST THE EDGE — verify the token is genuinely Google-signed and let the deployment's `run.invoker`
10
11
  * IAM restrict WHO (gcp-identity warns loudly, once, if the service is actually public and thus the
11
- * edge is NOT the gate). A non-empty list (`@WpAuthOidc('svc-a')`) enforces that explicit app-level
12
+ * edge is NOT the gate). A non-empty list (`oidc('svc-a')`) enforces that explicit app-level
12
13
  * allow-list as defense-in-depth. Off-GCP, gcp-identity mints + accepts a dev token so local dev needs
13
14
  * no GCP. Framework code reads NO process.env — an app that wants an env-driven allow-list binds an
14
15
  * {@link OidcHook} instead (env read at its composition root), keeping this default env-free and tests
@@ -17,5 +18,5 @@ import { GcpOidc } from '@webpieces/gcp-identity';
17
18
  export declare class DefaultOidcVerifier {
18
19
  private readonly gcpOidc;
19
20
  constructor(gcpOidc: GcpOidc);
20
- verify(token: string, callers: string[]): Promise<void>;
21
+ verify(token: string, callers: string[]): Promise<AuthenticatedMachineIdentity>;
21
22
  }
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
5
5
  const inversify_1 = require("inversify");
6
6
  const core_context_1 = require("@webpieces/core-context");
7
7
  const core_util_1 = require("@webpieces/core-util");
8
+ const AuthConfig_1 = require("./AuthConfig");
8
9
  const gcp_identity_1 = require("@webpieces/gcp-identity");
9
10
  /**
10
11
  * DefaultOidcVerifier - the framework's built-in Google OIDC verifier, injected into
@@ -12,10 +13,10 @@ const gcp_identity_1 = require("@webpieces/gcp-identity");
12
13
  * OIDC "just work" with ZERO wiring: http-routing depends on @webpieces/gcp-identity ON PURPOSE so a
13
14
  * server that binds nothing still verifies service-to-service OIDC.
14
15
  *
15
- * `verify` honors the endpoint's `@WpAuthOidc(...callers)` contract exactly: EMPTY (a bare `@WpAuthOidc()`)
16
+ * `verify` honors the endpoint's `oidc(...callers)` contract exactly: EMPTY (a bare `@WpAuth([oidc()])`)
16
17
  * = TRUST THE EDGE — verify the token is genuinely Google-signed and let the deployment's `run.invoker`
17
18
  * IAM restrict WHO (gcp-identity warns loudly, once, if the service is actually public and thus the
18
- * edge is NOT the gate). A non-empty list (`@WpAuthOidc('svc-a')`) enforces that explicit app-level
19
+ * edge is NOT the gate). A non-empty list (`oidc('svc-a')`) enforces that explicit app-level
19
20
  * allow-list as defense-in-depth. Off-GCP, gcp-identity mints + accepts a dev token so local dev needs
20
21
  * no GCP. Framework code reads NO process.env — an app that wants an env-driven allow-list binds an
21
22
  * {@link OidcHook} instead (env read at its composition root), keeping this default env-free and tests
@@ -27,13 +28,14 @@ let DefaultOidcVerifier = class DefaultOidcVerifier {
27
28
  this.gcpOidc = gcpOidc;
28
29
  }
29
30
  async verify(token, callers) {
30
- // callers pass straight through: EMPTY (@WpAuthOidc() with no callers) = TRUST THE EDGE, a
31
+ // callers pass straight through: EMPTY (@WpAuth([oidc()]) with no callers) = TRUST THE EDGE, a
31
32
  // non-empty list enforces the explicit allow-list. Do NOT inject a ['self'] default — that
32
33
  // would reject a legitimate cross-SA caller the edge already admitted.
33
34
  const result = await this.gcpOidc.verifyFromCallers(token, callers);
34
- if (!result.ok) {
35
+ if (!result.ok || !result.email) {
35
36
  throw new core_util_1.ApiUnauthorizedError(`OIDC rejected: ${result.reason ?? 'not an allowed caller'}`);
36
37
  }
38
+ return new AuthConfig_1.AuthenticatedMachineIdentity('oidc', result.email);
37
39
  }
38
40
  };
39
41
  exports.DefaultOidcVerifier = DefaultOidcVerifier;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultOidcVerifier.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/DefaultOidcVerifier.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,0DAAoE;AACpE,oDAA4D;AAC5D,0DAAkD;AAElD;;;;;;;;;;;;;;GAcG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAGU;IAFtC,YAEsC,OAAgB;QAAhB,YAAO,GAAP,OAAO,CAAS;IACnD,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,OAAiB;QACzC,2FAA2F;QAC3F,2FAA2F;QAC3F,uEAAuE;QACvE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACpE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACb,MAAM,IAAI,gCAAoB,CAC1B,kBAAkB,MAAM,CAAC,MAAM,IAAI,uBAAuB,EAAE,CAC/D,CAAC;QACN,CAAC;IACL,CAAC;CACJ,CAAA;AAjBY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,wCAAyB,GAAE;IAInB,mBAAA,IAAA,kBAAM,EAAC,sBAAO,CAAC,CAAA;6CAA2B,sBAAO;GAH7C,mBAAmB,CAiB/B","sourcesContent":["import { inject } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { ApiUnauthorizedError } from '@webpieces/core-util';\nimport { GcpOidc } 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` honors the endpoint's `@WpAuthOidc(...callers)` contract exactly: EMPTY (a bare `@WpAuthOidc()`)\n * = TRUST THE EDGE — verify the token is genuinely Google-signed and let the deployment's `run.invoker`\n * IAM restrict WHO (gcp-identity warns loudly, once, if the service is actually public and thus the\n * edge is NOT the gate). A non-empty list (`@WpAuthOidc('svc-a')`) enforces that explicit app-level\n * allow-list as defense-in-depth. Off-GCP, gcp-identity mints + accepts a dev token so local dev needs\n * no GCP. Framework code reads NO process.env — an app that wants an env-driven allow-list binds an\n * {@link OidcHook} instead (env read at its composition root), keeping this default env-free and tests\n * parallel-safe.\n */\n@provideFrameworkSingleton()\nexport class DefaultOidcVerifier {\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n ) {}\n\n async verify(token: string, callers: string[]): Promise<void> {\n // callers pass straight through: EMPTY (@WpAuthOidc() with no callers) = TRUST THE EDGE, a\n // non-empty list enforces the explicit allow-list. Do NOT inject a ['self'] default — that\n // would reject a legitimate cross-SA caller the edge already admitted.\n const result = await this.gcpOidc.verifyFromCallers(token, callers);\n if (!result.ok) {\n throw new ApiUnauthorizedError(\n `OIDC rejected: ${result.reason ?? 'not an allowed caller'}`,\n );\n }\n }\n}\n"]}
1
+ {"version":3,"file":"DefaultOidcVerifier.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/DefaultOidcVerifier.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,0DAAoE;AACpE,oDAA4D;AAC5D,6CAA4D;AAC5D,0DAAkD;AAElD;;;;;;;;;;;;;;GAcG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAGU;IAFtC,YAEsC,OAAgB;QAAhB,YAAO,GAAP,OAAO,CAAS;IACnD,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,OAAiB;QACzC,+FAA+F;QAC/F,2FAA2F;QAC3F,uEAAuE;QACvE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACpE,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAC9B,MAAM,IAAI,gCAAoB,CAC1B,kBAAkB,MAAM,CAAC,MAAM,IAAI,uBAAuB,EAAE,CAC/D,CAAC;QACN,CAAC;QACD,OAAO,IAAI,yCAA4B,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IAClE,CAAC;CACJ,CAAA;AAlBY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,wCAAyB,GAAE;IAInB,mBAAA,IAAA,kBAAM,EAAC,sBAAO,CAAC,CAAA;6CAA2B,sBAAO;GAH7C,mBAAmB,CAkB/B","sourcesContent":["import { inject } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { ApiUnauthorizedError } from '@webpieces/core-util';\nimport { AuthenticatedMachineIdentity } from './AuthConfig';\nimport { GcpOidc } 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` honors the endpoint's `oidc(...callers)` contract exactly: EMPTY (a bare `@WpAuth([oidc()])`)\n * = TRUST THE EDGE — verify the token is genuinely Google-signed and let the deployment's `run.invoker`\n * IAM restrict WHO (gcp-identity warns loudly, once, if the service is actually public and thus the\n * edge is NOT the gate). A non-empty list (`oidc('svc-a')`) enforces that explicit app-level\n * allow-list as defense-in-depth. Off-GCP, gcp-identity mints + accepts a dev token so local dev needs\n * no GCP. Framework code reads NO process.env — an app that wants an env-driven allow-list binds an\n * {@link OidcHook} instead (env read at its composition root), keeping this default env-free and tests\n * parallel-safe.\n */\n@provideFrameworkSingleton()\nexport class DefaultOidcVerifier {\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n ) {}\n\n async verify(token: string, callers: string[]): Promise<AuthenticatedMachineIdentity> {\n // callers pass straight through: EMPTY (@WpAuth([oidc()]) with no callers) = TRUST THE EDGE, a\n // non-empty list enforces the explicit allow-list. Do NOT inject a ['self'] default — that\n // would reject a legitimate cross-SA caller the edge already admitted.\n const result = await this.gcpOidc.verifyFromCallers(token, callers);\n if (!result.ok || !result.email) {\n throw new ApiUnauthorizedError(\n `OIDC rejected: ${result.reason ?? 'not an allowed caller'}`,\n );\n }\n return new AuthenticatedMachineIdentity('oidc', result.email);\n }\n}\n"]}
@@ -1,9 +1,9 @@
1
- import { AuthenticatedCaller } from './AuthConfig';
2
- /** In-process invocation credential. Never read from the wire or installed in ambient request state. */
1
+ import { RouteMetadata } from '@webpieces/core-util';
2
+ /** Token-free local entry, bound to the verified ingress scope and one actual endpoint. */
3
3
  export declare class InvocationAuthentication {
4
- readonly token: string;
5
- private readonly principal;
6
- constructor(token: string, principal: AuthenticatedCaller);
7
- /** Endpoint JWT verification must reproduce the ingress identity before endpoint policy runs. */
8
- assertCaller(caller: AuthenticatedCaller): void;
4
+ private readonly apiClass;
5
+ private readonly methodName;
6
+ private readonly scopeIdentity;
7
+ constructor(apiClass: Function, methodName: string);
8
+ assertTarget(route: RouteMetadata): void;
9
9
  }
@@ -1,26 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InvocationAuthentication = void 0;
4
- const node_util_1 = require("node:util");
4
+ const core_context_1 = require("@webpieces/core-context");
5
5
  const core_util_1 = require("@webpieces/core-util");
6
- /** In-process invocation credential. Never read from the wire or installed in ambient request state. */
6
+ const AuthenticatedCallerContext_1 = require("./AuthenticatedCallerContext");
7
+ /** Token-free local entry, bound to the verified ingress scope and one actual endpoint. */
7
8
  class InvocationAuthentication {
8
- token;
9
- principal;
10
- constructor(token, principal) {
11
- this.token = token;
12
- this.principal = principal;
9
+ apiClass;
10
+ methodName;
11
+ scopeIdentity;
12
+ constructor(apiClass, methodName) {
13
+ this.apiClass = apiClass;
14
+ this.methodName = methodName;
15
+ if (!AuthenticatedCallerContext_1.AuthenticatedCallerContext.hasEstablishedIngress()) {
16
+ throw new core_util_1.ApiImplementationError('Local invocation requires completed verified ingress.');
17
+ }
18
+ this.scopeIdentity = core_context_1.RequestContext.activeScopeIdentity();
13
19
  }
14
- /** Endpoint JWT verification must reproduce the ingress identity before endpoint policy runs. */
15
- assertCaller(caller) {
16
- const expected = this.principal;
17
- const sameRoles = caller.roles.length === expected.roles.length &&
18
- caller.roles.every((role) => expected.roles.includes(role));
19
- const sameEntries = caller.entries.length === expected.entries.length &&
20
- caller.entries.every((entry) => expected.entries.some((item) => item.key === entry.key && (0, node_util_1.isDeepStrictEqual)(item.value, entry.value)));
21
- const sameClaims = Object.keys(expected.claims).every((key) => (0, node_util_1.isDeepStrictEqual)(expected.claims[key], caller.claims[key]));
22
- if (caller.userId !== expected.userId || !sameRoles || !sameEntries || !sameClaims) {
23
- throw new core_util_1.ApiImplementationError('Invocation JWT disagrees with the verified ingress principal.');
20
+ assertTarget(route) {
21
+ if (this.scopeIdentity !== core_context_1.RequestContext.activeScopeIdentity() ||
22
+ !AuthenticatedCallerContext_1.AuthenticatedCallerContext.hasEstablishedIngress() ||
23
+ route.apiClass !== this.apiClass || route.methodName !== this.methodName) {
24
+ throw new core_util_1.ApiImplementationError('Local invocation authentication belongs to a different target or request scope.');
24
25
  }
25
26
  }
26
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"InvocationAuthentication.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/InvocationAuthentication.ts"],"names":[],"mappings":";;;AAAA,yCAA8C;AAC9C,oDAA8D;AAG9D,wGAAwG;AACxG,MAAa,wBAAwB;IAEb;IACC;IAFrB,YACoB,KAAa,EACZ,SAA8B;QAD/B,UAAK,GAAL,KAAK,CAAQ;QACZ,cAAS,GAAT,SAAS,CAAqB;IAChD,CAAC;IAEJ,iGAAiG;IACjG,YAAY,CAAC,MAA2B;QACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,MAAM,SAAS,GACX,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,KAAK,CAAC,MAAM;YAC7C,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QACxE,MAAM,WAAW,GACb,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,OAAO,CAAC,MAAM;YACjD,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAC3B,QAAQ,CAAC,OAAO,CAAC,IAAI,CACjB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,KAAK,KAAK,CAAC,GAAG,IAAI,IAAA,6BAAiB,EAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CACjF,CACJ,CAAC;QACN,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,GAAW,EAAE,EAAE,CAClE,IAAA,6BAAiB,EAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAC9D,CAAC;QACF,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW,IAAI,CAAC,UAAU,EAAE,CAAC;YACjF,MAAM,IAAI,kCAAsB,CAC5B,+DAA+D,CAClE,CAAC;QACN,CAAC;IACL,CAAC;CACJ;AA5BD,4DA4BC","sourcesContent":["import { isDeepStrictEqual } from 'node:util';\nimport { ApiImplementationError } from '@webpieces/core-util';\nimport { AuthenticatedCaller } from './AuthConfig';\n\n/** In-process invocation credential. Never read from the wire or installed in ambient request state. */\nexport class InvocationAuthentication {\n constructor(\n public readonly token: string,\n private readonly principal: AuthenticatedCaller,\n ) {}\n\n /** Endpoint JWT verification must reproduce the ingress identity before endpoint policy runs. */\n assertCaller(caller: AuthenticatedCaller): void {\n const expected = this.principal;\n const sameRoles =\n caller.roles.length === expected.roles.length &&\n caller.roles.every((role: string) => expected.roles.includes(role));\n const sameEntries =\n caller.entries.length === expected.entries.length &&\n caller.entries.every((entry) =>\n expected.entries.some(\n (item) => item.key === entry.key && isDeepStrictEqual(item.value, entry.value),\n ),\n );\n const sameClaims = Object.keys(expected.claims).every((key: string) =>\n isDeepStrictEqual(expected.claims[key], caller.claims[key]),\n );\n if (caller.userId !== expected.userId || !sameRoles || !sameEntries || !sameClaims) {\n throw new ApiImplementationError(\n 'Invocation JWT disagrees with the verified ingress principal.',\n );\n }\n }\n}\n"]}
1
+ {"version":3,"file":"InvocationAuthentication.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/InvocationAuthentication.ts"],"names":[],"mappings":";;;AAAA,0DAAyD;AACzD,oDAA6E;AAC7E,6EAA0E;AAE1E,2FAA2F;AAC3F,MAAa,wBAAwB;IAGJ;IAAqC;IAFjD,aAAa,CAAS;IAEvC,YAA6B,QAAkB,EAAmB,UAAkB;QAAvD,aAAQ,GAAR,QAAQ,CAAU;QAAmB,eAAU,GAAV,UAAU,CAAQ;QAChF,IAAI,CAAC,uDAA0B,CAAC,qBAAqB,EAAE,EAAE,CAAC;YACtD,MAAM,IAAI,kCAAsB,CAAC,uDAAuD,CAAC,CAAC;QAC9F,CAAC;QACD,IAAI,CAAC,aAAa,GAAG,6BAAc,CAAC,mBAAmB,EAAE,CAAC;IAC9D,CAAC;IAED,YAAY,CAAC,KAAoB;QAC7B,IAAI,IAAI,CAAC,aAAa,KAAK,6BAAc,CAAC,mBAAmB,EAAE;YAC3D,CAAC,uDAA0B,CAAC,qBAAqB,EAAE;YACnD,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3E,MAAM,IAAI,kCAAsB,CAAC,iFAAiF,CAAC,CAAC;QACxH,CAAC;IACL,CAAC;CACJ;AAjBD,4DAiBC","sourcesContent":["import { RequestContext } from '@webpieces/core-context';\nimport { ApiImplementationError, RouteMetadata } from '@webpieces/core-util';\nimport { AuthenticatedCallerContext } from './AuthenticatedCallerContext';\n\n/** Token-free local entry, bound to the verified ingress scope and one actual endpoint. */\nexport class InvocationAuthentication {\n private readonly scopeIdentity: object;\n\n constructor(private readonly apiClass: Function, private readonly methodName: string) {\n if (!AuthenticatedCallerContext.hasEstablishedIngress()) {\n throw new ApiImplementationError('Local invocation requires completed verified ingress.');\n }\n this.scopeIdentity = RequestContext.activeScopeIdentity();\n }\n\n assertTarget(route: RouteMetadata): void {\n if (this.scopeIdentity !== RequestContext.activeScopeIdentity() ||\n !AuthenticatedCallerContext.hasEstablishedIngress() ||\n route.apiClass !== this.apiClass || route.methodName !== this.methodName) {\n throw new ApiImplementationError('Local invocation authentication belongs to a different target or request scope.');\n }\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * COMPILE-TIME assertions that {@link JwtHook} mint/parse/authorize are async, mint is required, and
2
+ * COMPILE-TIME assertions that {@link JwtHook} mint/parse are async (authorization belongs to AuthorizationHook), mint is required, and
3
3
  * the old SYNC spelling no longer compiles. Each `@ts-expect-error` below FAILS THE BUILD (TS2578, "unused
4
4
  * '@ts-expect-error' directive") if the override it guards ever starts compiling again.
5
5
  *
@@ -4,7 +4,7 @@ exports.JwtHookCompileAssertions = void 0;
4
4
  const AuthHooks_1 = require("./AuthHooks");
5
5
  const AuthConfig_1 = require("./AuthConfig");
6
6
  /**
7
- * COMPILE-TIME assertions that {@link JwtHook} mint/parse/authorize are async, mint is required, and
7
+ * COMPILE-TIME assertions that {@link JwtHook} mint/parse are async (authorization belongs to AuthorizationHook), mint is required, and
8
8
  * the old SYNC spelling no longer compiles. Each `@ts-expect-error` below FAILS THE BUILD (TS2578, "unused
9
9
  * '@ts-expect-error' directive") if the override it guards ever starts compiling again.
10
10
  *
@@ -35,9 +35,6 @@ class JwtHookCompileAssertions {
35
35
  async parseJwt(_token) {
36
36
  return new AuthConfig_1.AuthenticatedCaller('u1');
37
37
  }
38
- async authorizeJwt(_values, _requirement) {
39
- // An implementation that needs no I/O simply has no await — that is allowed and free.
40
- }
41
38
  };
42
39
  class RemoteSigner {
43
40
  async sign(request) {
@@ -79,7 +76,7 @@ class JwtHookCompileAssertions {
79
76
  async parseJwt(_token) {
80
77
  return new AuthConfig_1.AuthenticatedCaller('u1');
81
78
  }
82
- // @ts-expect-error authorizeJwt is async now: a void override must not compile either
79
+ // @ts-expect-error authorizeJwt is removed; application policy belongs in AuthorizationHook
83
80
  authorizeJwt(_values, _requirement) {
84
81
  // an app rule enforced synchronously — the spelling this change deletes
85
82
  }
@@ -1 +1 @@
1
- {"version":3,"file":"JwtHookCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/JwtHookCompileAssertions.ts"],"names":[],"mappings":";;;AACA,2CAAiD;AACjD,6CAAmD;AAEnD;;;;;;;;;;;;;;;GAeG;AACH,MAAa,wBAAwB;IACjC,oFAAoF;IACpF,UAAU;QACN,MAAM,sBAAsB;YACI;YAA5B,YAA4B,gBAAwB;gBAAxB,qBAAgB,GAAhB,gBAAgB,CAAQ;YAAG,CAAC;SAC3D;QACD,KAAK,KAAM,SAAQ,mBAA+B;YACrC,KAAK,CAAC,IAAI,CAAC,OAA+B;gBAC/C,OAAO,IAAI,qBAAS,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;YAC3E,CAAC;YAEQ,KAAK,CAAC,QAAQ,CAAC,MAAc;gBAClC,OAAO,IAAI,gCAAmB,CAAC,IAAI,CAAC,CAAC;YACzC,CAAC;YAEQ,KAAK,CAAC,YAAY,CAAC,OAA4B,EAAE,YAA4B;gBAClF,sFAAsF;YAC1F,CAAC;SACJ,CAAC;QAEF,MAAM,YAAY;YACd,KAAK,CAAC,IAAI,CAAC,OAA+B;gBACtC,OAAO,IAAI,qBAAS,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;YAC3E,CAAC;SACJ;QACD,MAAM,YAAY,GAAG,IAAI,YAAY,EAAE,CAAC;QACxC,KAAK,KAAM,SAAQ,mBAA+B;YACrC,IAAI,CAAC,OAA+B;gBACzC,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtC,CAAC;YAEQ,KAAK,CAAC,QAAQ,CAAC,MAAc;gBAClC,OAAO,IAAI,gCAAmB,CAAC,WAAW,CAAC,CAAC;YAChD,CAAC;SACJ,CAAC;IACN,CAAC;IAED,yFAAyF;IACzF,QAAQ;QACJ,8DAA8D;QAC9D,KAAK,KAAM,SAAQ,mBAAe;YAC9B,iIAAiI;YACxH,QAAQ,CAAC,MAAc;gBAC5B,OAAO,IAAI,gCAAmB,CAAC,IAAI,CAAC,CAAC;YACzC,CAAC;SACJ,CAAC;QACF,KAAK,KAAM,SAAQ,mBAAe;YAC9B,2GAA2G;YAClG,IAAI,CAAC,OAAe;gBACzB,OAAO,IAAI,qBAAS,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;YAC1D,CAAC;YAEQ,KAAK,CAAC,QAAQ,CAAC,MAAc;gBAClC,OAAO,IAAI,gCAAmB,CAAC,IAAI,CAAC,CAAC;YACzC,CAAC;SACJ,CAAC;QACF,KAAK,KAAM,SAAQ,mBAAe;YACrB,KAAK,CAAC,IAAI,CAAC,OAAe;gBAC/B,OAAO,IAAI,qBAAS,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;YAC1D,CAAC;YAEQ,KAAK,CAAC,QAAQ,CAAC,MAAc;gBAClC,OAAO,IAAI,gCAAmB,CAAC,IAAI,CAAC,CAAC;YACzC,CAAC;YAED,sFAAsF;YAC7E,YAAY,CAAC,OAA4B,EAAE,YAA4B;gBAC5E,wEAAwE;YAC5E,CAAC;SACJ,CAAC;IACN,CAAC;CACJ;AAvED,4DAuEC","sourcesContent":["import { JwtRequirement } from '@webpieces/core-util';\nimport { JwtHook, MintedJwt } from './AuthHooks';\nimport { AuthenticatedCaller } from './AuthConfig';\n\n/**\n * COMPILE-TIME assertions that {@link JwtHook} mint/parse/authorize are async, mint is required, and\n * the old SYNC spelling no longer compiles. Each `@ts-expect-error` below FAILS THE BUILD (TS2578, \"unused\n * '@ts-expect-error' directive\") if the override it guards ever starts compiling again.\n *\n * WHY THIS IS NOT A `.spec.ts` FILE — the same reason as `core-util`'s `AuthJwtCompileAssertions.ts`:\n * tsconfig.lib.json EXCLUDES specs and vitest strips types with esbuild, so a `@ts-expect-error` in a\n * spec is inert and the suite passes either way. A type-level guarantee has to be asserted in a file\n * the type-checker actually compiles.\n *\n * WHY IT MATTERS HERE SPECIFICALLY. This repo ships no backwards-compatibility shims, so making the\n * hook async means every existing implementor's sync override must STOP COMPILING — the compile error\n * IS the migration. A sync `parseJwt` that kept compiling would be silently awaited to the same value\n * and the break would look optional, which is precisely how an old spelling survives. Pinning both\n * directions here means the async-ness cannot be quietly relaxed later either.\n */\nexport class JwtHookCompileAssertions {\n /** The async spellings must keep compiling; asserted by the ABSENCE of an error. */\n legitimate(): void {\n class ApplicationMintRequest {\n constructor(public readonly applicationClaim: string) {}\n }\n void class extends JwtHook<ApplicationMintRequest> {\n override async mint(request: ApplicationMintRequest): Promise<MintedJwt> {\n return new MintedJwt(request.applicationClaim, Date.now() / 1000 + 60);\n }\n\n override async parseJwt(_token: string): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('u1');\n }\n\n override async authorizeJwt(_values: AuthenticatedCaller, _requirement: JwtRequirement): Promise<void> {\n // An implementation that needs no I/O simply has no await — that is allowed and free.\n }\n };\n\n class RemoteSigner {\n async sign(request: ApplicationMintRequest): Promise<MintedJwt> {\n return new MintedJwt(request.applicationClaim, Date.now() / 1000 + 60);\n }\n }\n const remoteSigner = new RemoteSigner();\n void class extends JwtHook<ApplicationMintRequest> {\n override mint(request: ApplicationMintRequest): Promise<MintedJwt> {\n return remoteSigner.sign(request);\n }\n\n override async parseJwt(_token: string): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('remote-u1');\n }\n };\n }\n\n /** The SYNC spellings — what every implementor wrote before — must now be UNWRITABLE. */\n rejected(): void {\n // @ts-expect-error mint is a required compile-time obligation\n void class extends JwtHook<string> {\n // @ts-expect-error parseJwt is async now: returning AuthenticatedCaller instead of Promise<AuthenticatedCaller> must not compile\n override parseJwt(_token: string): AuthenticatedCaller {\n return new AuthenticatedCaller('u1');\n }\n };\n void class extends JwtHook<string> {\n // @ts-expect-error mint is always async: a synchronous signer returns Promise.resolve through the contract\n override mint(request: string): MintedJwt {\n return new MintedJwt(request, Date.now() / 1000 + 60);\n }\n\n override async parseJwt(_token: string): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('u1');\n }\n };\n void class extends JwtHook<string> {\n override async mint(request: string): Promise<MintedJwt> {\n return new MintedJwt(request, Date.now() / 1000 + 60);\n }\n\n override async parseJwt(_token: string): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('u1');\n }\n\n // @ts-expect-error authorizeJwt is async now: a void override must not compile either\n override authorizeJwt(_values: AuthenticatedCaller, _requirement: JwtRequirement): void {\n // an app rule enforced synchronously — the spelling this change deletes\n }\n };\n }\n}\n"]}
1
+ {"version":3,"file":"JwtHookCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/JwtHookCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,2CAAiD;AACjD,6CAAmD;AAEnD;;;;;;;;;;;;;;;GAeG;AACH,MAAa,wBAAwB;IACjC,oFAAoF;IACpF,UAAU;QACN,MAAM,sBAAsB;YACI;YAA5B,YAA4B,gBAAwB;gBAAxB,qBAAgB,GAAhB,gBAAgB,CAAQ;YAAG,CAAC;SAC3D;QACD,KAAK,KAAM,SAAQ,mBAA+B;YACrC,KAAK,CAAC,IAAI,CAAC,OAA+B;gBAC/C,OAAO,IAAI,qBAAS,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;YAC3E,CAAC;YAEQ,KAAK,CAAC,QAAQ,CAAC,MAAc;gBAClC,OAAO,IAAI,gCAAmB,CAAC,IAAI,CAAC,CAAC;YACzC,CAAC;SAGJ,CAAC;QAEF,MAAM,YAAY;YACd,KAAK,CAAC,IAAI,CAAC,OAA+B;gBACtC,OAAO,IAAI,qBAAS,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;YAC3E,CAAC;SACJ;QACD,MAAM,YAAY,GAAG,IAAI,YAAY,EAAE,CAAC;QACxC,KAAK,KAAM,SAAQ,mBAA+B;YACrC,IAAI,CAAC,OAA+B;gBACzC,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtC,CAAC;YAEQ,KAAK,CAAC,QAAQ,CAAC,MAAc;gBAClC,OAAO,IAAI,gCAAmB,CAAC,WAAW,CAAC,CAAC;YAChD,CAAC;SACJ,CAAC;IACN,CAAC;IAED,yFAAyF;IACzF,QAAQ;QACJ,8DAA8D;QAC9D,KAAK,KAAM,SAAQ,mBAAe;YAC9B,iIAAiI;YACxH,QAAQ,CAAC,MAAc;gBAC5B,OAAO,IAAI,gCAAmB,CAAC,IAAI,CAAC,CAAC;YACzC,CAAC;SACJ,CAAC;QACF,KAAK,KAAM,SAAQ,mBAAe;YAC9B,2GAA2G;YAClG,IAAI,CAAC,OAAe;gBACzB,OAAO,IAAI,qBAAS,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;YAC1D,CAAC;YAEQ,KAAK,CAAC,QAAQ,CAAC,MAAc;gBAClC,OAAO,IAAI,gCAAmB,CAAC,IAAI,CAAC,CAAC;YACzC,CAAC;SACJ,CAAC;QACF,KAAK,KAAM,SAAQ,mBAAe;YACrB,KAAK,CAAC,IAAI,CAAC,OAAe;gBAC/B,OAAO,IAAI,qBAAS,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;YAC1D,CAAC;YAEQ,KAAK,CAAC,QAAQ,CAAC,MAAc;gBAClC,OAAO,IAAI,gCAAmB,CAAC,IAAI,CAAC,CAAC;YACzC,CAAC;YAED,4FAA4F;YACnF,YAAY,CAAC,OAA4B,EAAE,YAAoB;gBACpE,wEAAwE;YAC5E,CAAC;SACJ,CAAC;IACN,CAAC;CACJ;AArED,4DAqEC","sourcesContent":["import { JwtHook, MintedJwt } from './AuthHooks';\nimport { AuthenticatedCaller } from './AuthConfig';\n\n/**\n * COMPILE-TIME assertions that {@link JwtHook} mint/parse are async (authorization belongs to AuthorizationHook), mint is required, and\n * the old SYNC spelling no longer compiles. Each `@ts-expect-error` below FAILS THE BUILD (TS2578, \"unused\n * '@ts-expect-error' directive\") if the override it guards ever starts compiling again.\n *\n * WHY THIS IS NOT A `.spec.ts` FILE — the same reason as `core-util`'s `AuthJwtCompileAssertions.ts`:\n * tsconfig.lib.json EXCLUDES specs and vitest strips types with esbuild, so a `@ts-expect-error` in a\n * spec is inert and the suite passes either way. A type-level guarantee has to be asserted in a file\n * the type-checker actually compiles.\n *\n * WHY IT MATTERS HERE SPECIFICALLY. This repo ships no backwards-compatibility shims, so making the\n * hook async means every existing implementor's sync override must STOP COMPILING — the compile error\n * IS the migration. A sync `parseJwt` that kept compiling would be silently awaited to the same value\n * and the break would look optional, which is precisely how an old spelling survives. Pinning both\n * directions here means the async-ness cannot be quietly relaxed later either.\n */\nexport class JwtHookCompileAssertions {\n /** The async spellings must keep compiling; asserted by the ABSENCE of an error. */\n legitimate(): void {\n class ApplicationMintRequest {\n constructor(public readonly applicationClaim: string) {}\n }\n void class extends JwtHook<ApplicationMintRequest> {\n override async mint(request: ApplicationMintRequest): Promise<MintedJwt> {\n return new MintedJwt(request.applicationClaim, Date.now() / 1000 + 60);\n }\n\n override async parseJwt(_token: string): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('u1');\n }\n\n\n };\n\n class RemoteSigner {\n async sign(request: ApplicationMintRequest): Promise<MintedJwt> {\n return new MintedJwt(request.applicationClaim, Date.now() / 1000 + 60);\n }\n }\n const remoteSigner = new RemoteSigner();\n void class extends JwtHook<ApplicationMintRequest> {\n override mint(request: ApplicationMintRequest): Promise<MintedJwt> {\n return remoteSigner.sign(request);\n }\n\n override async parseJwt(_token: string): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('remote-u1');\n }\n };\n }\n\n /** The SYNC spellings — what every implementor wrote before — must now be UNWRITABLE. */\n rejected(): void {\n // @ts-expect-error mint is a required compile-time obligation\n void class extends JwtHook<string> {\n // @ts-expect-error parseJwt is async now: returning AuthenticatedCaller instead of Promise<AuthenticatedCaller> must not compile\n override parseJwt(_token: string): AuthenticatedCaller {\n return new AuthenticatedCaller('u1');\n }\n };\n void class extends JwtHook<string> {\n // @ts-expect-error mint is always async: a synchronous signer returns Promise.resolve through the contract\n override mint(request: string): MintedJwt {\n return new MintedJwt(request, Date.now() / 1000 + 60);\n }\n\n override async parseJwt(_token: string): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('u1');\n }\n };\n void class extends JwtHook<string> {\n override async mint(request: string): Promise<MintedJwt> {\n return new MintedJwt(request, Date.now() / 1000 + 60);\n }\n\n override async parseJwt(_token: string): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('u1');\n }\n\n // @ts-expect-error authorizeJwt is removed; application policy belongs in AuthorizationHook\n override authorizeJwt(_values: AuthenticatedCaller, _requirement: object): void {\n // an app rule enforced synchronously — the spelling this change deletes\n }\n };\n }\n}\n"]}
@@ -6,6 +6,7 @@ const core_context_1 = require("@webpieces/core-context");
6
6
  const WpResponse_1 = require("./WpResponse");
7
7
  const FilterMatcher_1 = require("./FilterMatcher");
8
8
  const core_util_1 = require("@webpieces/core-util");
9
+ const AuthorizationHook_1 = require("./AuthorizationHook");
9
10
  const log = core_util_1.LogManager.getLogger('RouteBuilder');
10
11
  /**
11
12
  * FilterWithMeta - Pairs a resolved filter instance with its definition.
@@ -107,6 +108,12 @@ let RouteBuilderImpl = class RouteBuilderImpl {
107
108
  * @param route - Route definition with controller class and method name
108
109
  */
109
110
  addRoute(route) {
111
+ const policy = route.routeMeta.authorization;
112
+ if (!route.routeMeta.authMeta || !policy)
113
+ throw new Error(`Endpoint ${route.routeMeta.path} requires @WpAuth/@WpAuthPublic and @WpAuthorization.`);
114
+ if (!this.container)
115
+ throw new Error('Set the route container before registering endpoints.');
116
+ this.container.get(AuthorizationHook_1.AuthorizationService).validate(policy);
110
117
  const routeWithMeta = this.createRouteHandlerWithMeta(route);
111
118
  this.routes.push(routeWithMeta);
112
119
  // Also add to map for O(1) lookup by method:path
@@ -1 +1 @@
1
- {"version":3,"file":"RouteBuilderImpl.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/RouteBuilderImpl.ts"],"names":[],"mappings":"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","sourcesContent":["import { Container } from 'inversify';\nimport { RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { RouteHandler } from './RouteHandler';\nimport { MethodMeta } from './MethodMeta';\nimport { RouteMetadata, DocumentDesign } from '@webpieces/core-util';\nimport { Service } from '@webpieces/core-util';\nimport { WpResponse } from './WpResponse';\nimport { FilterMatcher, HttpFilter } from './FilterMatcher';\nimport { LogManager } from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('RouteBuilder');\n\ninterface NamedClass {\n name?: string;\n}\n\n/**\n * FilterWithMeta - Pairs a resolved filter instance with its definition.\n * Stores both the DI-resolved filter and the metadata needed for matching.\n */\nexport class FilterWithMeta {\n constructor(\n public filter: HttpFilter,\n public definition: FilterDefinition,\n ) {}\n}\n\n/**\n * RouteHandlerImpl - Concrete implementation of RouteHandler.\n * Wraps a resolved controller and method to invoke on each request.\n */\nexport class RouteHandlerImpl<TResult> implements RouteHandler<TResult> {\n constructor(\n private controller: Record<string, unknown>,\n // webpieces-disable no-any-unknown -- route handlers invoke arbitrary API argument shapes\n private method: (this: unknown, ...args: unknown[]) => Promise<TResult>,\n ) {}\n\n async execute(meta: MethodMeta): Promise<TResult> {\n // Invoke the method with requestDto from meta\n // The controller is already resolved - no DI lookup on every request!\n const result: TResult = await this.method.apply(this.controller, [...meta.requestArgs]);\n return result;\n }\n}\n/**\n * RouteHandlerWithMeta - Pairs a route handler with its definition.\n * Stores both the handler (which wraps the DI-resolved controller) and the route metadata.\n *\n * We use unknown for the generic type since we store different TResult types in the same Map.\n * Type safety is maintained through the generic on RouteDefinition at registration time.\n */\nexport class RouteHandlerWithMeta {\n constructor(\n public invokeControllerHandler: RouteHandler<unknown>,\n public definition: RouteDefinition,\n ) {}\n}\n\n/**\n * RouteBuilderImpl - Concrete implementation of RouteBuilder interface.\n *\n * Similar to Java WebPieces RouteBuilder, this class is responsible for:\n * 1. Registering routes with their handlers\n * 2. Registering filters with priority\n *\n * This class is explicit (not anonymous) to:\n * - Improve traceability and debugging\n * - Make the code easier to understand\n * - Enable better IDE navigation (Cmd+Click on addRoute works!)\n *\n * DI Pattern: This class is registered in webpiecesContainer via @provideFrameworkSingleton()\n * but needs appContainer to resolve filters/controllers. The container is set via\n * setContainer() after appContainer is created (late binding pattern).\n */\n@provideFrameworkSingleton()\nexport class RouteBuilderImpl implements RouteBuilder {\n private routes: RouteHandlerWithMeta[] = [];\n private filterRegistry: Array<FilterWithMeta> = [];\n private container?: Container;\n\n /**\n * Map for O(1) route lookup by method:path.\n * Used by both addRoute() and createRouteInvoker() for fast route access.\n */\n private routeMap: Map<string, RouteHandlerWithMeta> = new Map();\n\n /**\n * Create route key for consistent lookup.\n * Key format: \"${METHOD}:${path}\" (e.g., \"POST:/search/item\")\n */\n private createRouteKey(method: string, path: string): string {\n return `${method.toUpperCase()}:${path}`;\n }\n\n /**\n * Set the DI container used for resolving filters and controllers.\n * Called by WebpiecesCoreServer after appContainer is created.\n *\n * @param container - The application DI container (appContainer)\n */\n setContainer(container: Container): void {\n this.container = container;\n }\n\n /**\n * Register a route with the router.\n *\n * Uses createRouteHandlerWithMeta() to create the handler, then stores it\n * in both the routes array and the routeMap for O(1) lookup.\n *\n * @param route - Route definition with controller class and method name\n */\n addRoute(route: RouteDefinition): void {\n const routeWithMeta = this.createRouteHandlerWithMeta(route);\n this.routes.push(routeWithMeta);\n\n // Also add to map for O(1) lookup by method:path\n const key = this.createRouteKey(route.routeMeta.httpMethod, route.routeMeta.path);\n this.routeMap.set(key, routeWithMeta);\n }\n\n /**\n * Create RouteHandlerWithMeta from a RouteDefinition.\n *\n * Resolves controller from DI container ONCE and creates a handler that\n * invokes the controller method with the request DTO.\n *\n * This method is used by:\n * - addRoute() for production route registration\n * - createRouteInvoker() for test clients (via createApiClient)\n *\n * @param route - Route definition with controller class and method name\n * @returns RouteHandlerWithMeta containing the handler and route definition\n */\n private createRouteHandlerWithMeta<TResult = unknown>(\n route: RouteDefinition,\n ): RouteHandlerWithMeta {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering routes.');\n }\n\n const routeMeta = route.routeMeta;\n\n // Resolve controller instance from DI container ONCE (not on every request!)\n const controller = this.container.get(route.controllerClass) as Record<string, unknown>;\n\n // Get the controller method\n const method = controller[routeMeta.methodName];\n if (typeof method !== 'function') {\n const controllerName = (route.controllerClass as NamedClass).name || 'Unknown';\n throw new Error(\n `Method ${routeMeta.methodName} not found on controller ${controllerName}`,\n );\n }\n\n const handler = new RouteHandlerImpl<TResult>(\n controller,\n // webpieces-disable no-any-unknown -- resolved controller method has the registered API's arguments\n method as (this: unknown, ...args: unknown[]) => Promise<TResult>,\n );\n\n // Return handler with route definition\n // webpieces-disable no-any-unknown -- heterogeneous handlers share one registration collection\n return new RouteHandlerWithMeta(handler as RouteHandler<unknown>, route);\n }\n\n /**\n * Register a filter with the filter chain.\n *\n * Resolves the filter from DI container and pairs it with the filter definition.\n * The definition includes pattern information used for route-specific filtering.\n *\n * @param filterDef - Filter definition with priority, filter class, and optional filepath pattern\n */\n addFilter(filterDef: FilterDefinition): void {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering filters.');\n }\n\n // Resolve filter instance from DI container\n const filter = this.container.get<HttpFilter>(filterDef.filterClass);\n\n // Store filter with its definition\n const filterWithMeta = new FilterWithMeta(filter, filterDef);\n this.filterRegistry.push(filterWithMeta);\n }\n\n /**\n * Get all registered routes.\n *\n * @returns Map of routes with handlers and definitions, keyed by \"METHOD:path\"\n */\n getRoutes(): RouteHandlerWithMeta[] {\n return this.routes;\n }\n\n /**\n * Get all filters sorted by priority (highest priority first).\n *\n * @returns Array of FilterWithMeta sorted by priority\n */\n getSortedFilters(): Array<FilterWithMeta> {\n return [...this.filterRegistry].sort(\n (a, b) => b.definition.priority - a.definition.priority,\n );\n }\n\n /**\n * Cached filter definitions for lazy route setup.\n */\n private cachedFilterDefinitions?: FilterDefinition[];\n\n /**\n * Get filter definitions, computing once and caching.\n */\n private getFilterDefinitions(): FilterDefinition[] {\n if (!this.cachedFilterDefinitions) {\n const sortedFilters = this.getSortedFilters();\n this.cachedFilterDefinitions = sortedFilters.map((fwm) => {\n const def = fwm.definition;\n def.filter = fwm.filter;\n return def;\n });\n }\n return this.cachedFilterDefinitions;\n }\n\n /**\n * Setup a single route by creating its filter chain.\n * This is called lazily by createHandler() and getRouteService().\n *\n * Creates a Service that wraps the filter chain and controller invocation.\n * The service is DTO-only and has no Express dependency.\n *\n * @param key - Route key in format \"METHOD:path\"\n * @param routeWithMeta - Route handler with metadata\n * @returns The service for this route\n */\n public createRouteHandler(\n routeWithMeta: RouteHandlerWithMeta,\n ): Service<MethodMeta, WpResponse<unknown>> {\n const route = routeWithMeta.definition;\n const routeMeta = route.routeMeta;\n\n log.info(`Setting up route: ${routeMeta.httpMethod} ${routeMeta.path}`);\n\n // ONE chain for both HTTP and in-process — no transport tier. The fixed framework\n // filters (LogApiFilter, AuthFilter) are auto-installed and read the transport-neutral\n // HttpRequest, so they run identically in both.\n const filterDefinitions = this.getFilterDefinitions();\n\n // Find matching filters for this route\n const matchingFilters = FilterMatcher.findMatchingFilters(\n route.controllerFilepath,\n filterDefinitions,\n );\n\n // Create service that wraps the controller execution\n const controllerService: Service<MethodMeta, WpResponse<unknown>> = {\n invoke: async (meta: MethodMeta): Promise<WpResponse<unknown>> => {\n const result = await routeWithMeta.invokeControllerHandler.execute(meta);\n // A void endpoint (e.g. a @PubSub cloud-task handler returning Promise<void>)\n // yields undefined; coerce to {} so the response is a non-null JSON body\n // (downstream LogApiCall/serialization require one), mirroring `result ?? {}`.\n return new WpResponse(result ?? {});\n },\n };\n\n if (matchingFilters.length === 0) {\n throw new Error(\n 'No filters found for route — the framework auto-installs LogApiFilter + AuthFilter, so this indicates a wiring problem.',\n );\n }\n\n // Chain filters: highest priority (first in array) should run first (be outermost)\n // Build from innermost (lowest priority) to outermost (highest priority)\n // Start with controller, then wrap with filters in reverse priority order\n let service: Service<MethodMeta, WpResponse<unknown>> = controllerService;\n for (let i = matchingFilters.length - 1; i >= 0; i--) {\n service = matchingFilters[i].chainService(service);\n }\n\n return service;\n }\n\n /**\n * Create an invoker function for a route (for testing via createApiClient).\n * Uses routeMap for O(1) lookup, sets up the filter chain ONCE,\n * and returns a Service that can be called multiple times without\n * recreating the filter chain.\n *\n * This method is called by WebpiecesServer.createApiClient() during proxy setup.\n * The returned Service is stored as the proxy method and invoked on each call.\n *\n * @param method - HTTP method (GET, POST, etc.)\n * @param path - URL path\n * @returns A Service that invokes the route\n */\n createRouteInvoker(method: string, path: string): Service<MethodMeta, WpResponse<unknown>> {\n // Use routeMap for O(1) lookup (not linear search!)\n const key = this.createRouteKey(method, path);\n const routeWithMeta = this.routeMap.get(key);\n\n if (!routeWithMeta) {\n throw new Error(`Route not found: ${method} ${path}`);\n }\n\n // Setup filter chain ONCE (not on every invocation!). Same chain as HTTP — auth included.\n return this.createRouteHandler(routeWithMeta);\n }\n\n /**\n * Look up the RouteMetadata (incl. authMeta) for a registered route by method+path.\n * Used to build a MethodMeta for an in-process dispatch (e.g. a delivered cloud\n * task) so the filter chain sees the same routeMeta production HTTP would.\n *\n * @returns the route's RouteMetadata, or undefined if no route is registered.\n */\n getRouteMeta(method: string, path: string): RouteMetadata | undefined {\n const key = this.createRouteKey(method, path);\n return this.routeMap.get(key)?.definition.routeMeta;\n }\n}\n"]}
1
+ {"version":3,"file":"RouteBuilderImpl.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/RouteBuilderImpl.ts"],"names":[],"mappings":"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","sourcesContent":["import { Container } from 'inversify';\nimport { RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { RouteHandler } from './RouteHandler';\nimport { MethodMeta } from './MethodMeta';\nimport { RouteMetadata, DocumentDesign } from '@webpieces/core-util';\nimport { Service } from '@webpieces/core-util';\nimport { WpResponse } from './WpResponse';\nimport { FilterMatcher, HttpFilter } from './FilterMatcher';\nimport { LogManager } from '@webpieces/core-util';\nimport { AuthorizationService } from './AuthorizationHook';\n\nconst log = LogManager.getLogger('RouteBuilder');\n\ninterface NamedClass {\n name?: string;\n}\n\n/**\n * FilterWithMeta - Pairs a resolved filter instance with its definition.\n * Stores both the DI-resolved filter and the metadata needed for matching.\n */\nexport class FilterWithMeta {\n constructor(\n public filter: HttpFilter,\n public definition: FilterDefinition,\n ) {}\n}\n\n/**\n * RouteHandlerImpl - Concrete implementation of RouteHandler.\n * Wraps a resolved controller and method to invoke on each request.\n */\nexport class RouteHandlerImpl<TResult> implements RouteHandler<TResult> {\n constructor(\n private controller: Record<string, unknown>,\n // webpieces-disable no-any-unknown -- route handlers invoke arbitrary API argument shapes\n private method: (this: unknown, ...args: unknown[]) => Promise<TResult>,\n ) {}\n\n async execute(meta: MethodMeta): Promise<TResult> {\n // Invoke the method with requestDto from meta\n // The controller is already resolved - no DI lookup on every request!\n const result: TResult = await this.method.apply(this.controller, [...meta.requestArgs]);\n return result;\n }\n}\n/**\n * RouteHandlerWithMeta - Pairs a route handler with its definition.\n * Stores both the handler (which wraps the DI-resolved controller) and the route metadata.\n *\n * We use unknown for the generic type since we store different TResult types in the same Map.\n * Type safety is maintained through the generic on RouteDefinition at registration time.\n */\nexport class RouteHandlerWithMeta {\n constructor(\n public invokeControllerHandler: RouteHandler<unknown>,\n public definition: RouteDefinition,\n ) {}\n}\n\n/**\n * RouteBuilderImpl - Concrete implementation of RouteBuilder interface.\n *\n * Similar to Java WebPieces RouteBuilder, this class is responsible for:\n * 1. Registering routes with their handlers\n * 2. Registering filters with priority\n *\n * This class is explicit (not anonymous) to:\n * - Improve traceability and debugging\n * - Make the code easier to understand\n * - Enable better IDE navigation (Cmd+Click on addRoute works!)\n *\n * DI Pattern: This class is registered in webpiecesContainer via @provideFrameworkSingleton()\n * but needs appContainer to resolve filters/controllers. The container is set via\n * setContainer() after appContainer is created (late binding pattern).\n */\n@provideFrameworkSingleton()\nexport class RouteBuilderImpl implements RouteBuilder {\n private routes: RouteHandlerWithMeta[] = [];\n private filterRegistry: Array<FilterWithMeta> = [];\n private container?: Container;\n\n /**\n * Map for O(1) route lookup by method:path.\n * Used by both addRoute() and createRouteInvoker() for fast route access.\n */\n private routeMap: Map<string, RouteHandlerWithMeta> = new Map();\n\n /**\n * Create route key for consistent lookup.\n * Key format: \"${METHOD}:${path}\" (e.g., \"POST:/search/item\")\n */\n private createRouteKey(method: string, path: string): string {\n return `${method.toUpperCase()}:${path}`;\n }\n\n /**\n * Set the DI container used for resolving filters and controllers.\n * Called by WebpiecesCoreServer after appContainer is created.\n *\n * @param container - The application DI container (appContainer)\n */\n setContainer(container: Container): void {\n this.container = container;\n }\n\n /**\n * Register a route with the router.\n *\n * Uses createRouteHandlerWithMeta() to create the handler, then stores it\n * in both the routes array and the routeMap for O(1) lookup.\n *\n * @param route - Route definition with controller class and method name\n */\n addRoute(route: RouteDefinition): void {\n const policy = route.routeMeta.authorization;\n if (!route.routeMeta.authMeta || !policy) throw new Error(`Endpoint ${route.routeMeta.path} requires @WpAuth/@WpAuthPublic and @WpAuthorization.`);\n if (!this.container) throw new Error('Set the route container before registering endpoints.');\n this.container.get(AuthorizationService).validate(policy);\n const routeWithMeta = this.createRouteHandlerWithMeta(route);\n this.routes.push(routeWithMeta);\n\n // Also add to map for O(1) lookup by method:path\n const key = this.createRouteKey(route.routeMeta.httpMethod, route.routeMeta.path);\n this.routeMap.set(key, routeWithMeta);\n }\n\n /**\n * Create RouteHandlerWithMeta from a RouteDefinition.\n *\n * Resolves controller from DI container ONCE and creates a handler that\n * invokes the controller method with the request DTO.\n *\n * This method is used by:\n * - addRoute() for production route registration\n * - createRouteInvoker() for test clients (via createApiClient)\n *\n * @param route - Route definition with controller class and method name\n * @returns RouteHandlerWithMeta containing the handler and route definition\n */\n private createRouteHandlerWithMeta<TResult = unknown>(\n route: RouteDefinition,\n ): RouteHandlerWithMeta {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering routes.');\n }\n\n const routeMeta = route.routeMeta;\n\n // Resolve controller instance from DI container ONCE (not on every request!)\n const controller = this.container.get(route.controllerClass) as Record<string, unknown>;\n\n // Get the controller method\n const method = controller[routeMeta.methodName];\n if (typeof method !== 'function') {\n const controllerName = (route.controllerClass as NamedClass).name || 'Unknown';\n throw new Error(\n `Method ${routeMeta.methodName} not found on controller ${controllerName}`,\n );\n }\n\n const handler = new RouteHandlerImpl<TResult>(\n controller,\n // webpieces-disable no-any-unknown -- resolved controller method has the registered API's arguments\n method as (this: unknown, ...args: unknown[]) => Promise<TResult>,\n );\n\n // Return handler with route definition\n // webpieces-disable no-any-unknown -- heterogeneous handlers share one registration collection\n return new RouteHandlerWithMeta(handler as RouteHandler<unknown>, route);\n }\n\n /**\n * Register a filter with the filter chain.\n *\n * Resolves the filter from DI container and pairs it with the filter definition.\n * The definition includes pattern information used for route-specific filtering.\n *\n * @param filterDef - Filter definition with priority, filter class, and optional filepath pattern\n */\n addFilter(filterDef: FilterDefinition): void {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering filters.');\n }\n\n // Resolve filter instance from DI container\n const filter = this.container.get<HttpFilter>(filterDef.filterClass);\n\n // Store filter with its definition\n const filterWithMeta = new FilterWithMeta(filter, filterDef);\n this.filterRegistry.push(filterWithMeta);\n }\n\n /**\n * Get all registered routes.\n *\n * @returns Map of routes with handlers and definitions, keyed by \"METHOD:path\"\n */\n getRoutes(): RouteHandlerWithMeta[] {\n return this.routes;\n }\n\n /**\n * Get all filters sorted by priority (highest priority first).\n *\n * @returns Array of FilterWithMeta sorted by priority\n */\n getSortedFilters(): Array<FilterWithMeta> {\n return [...this.filterRegistry].sort(\n (a, b) => b.definition.priority - a.definition.priority,\n );\n }\n\n /**\n * Cached filter definitions for lazy route setup.\n */\n private cachedFilterDefinitions?: FilterDefinition[];\n\n /**\n * Get filter definitions, computing once and caching.\n */\n private getFilterDefinitions(): FilterDefinition[] {\n if (!this.cachedFilterDefinitions) {\n const sortedFilters = this.getSortedFilters();\n this.cachedFilterDefinitions = sortedFilters.map((fwm) => {\n const def = fwm.definition;\n def.filter = fwm.filter;\n return def;\n });\n }\n return this.cachedFilterDefinitions;\n }\n\n /**\n * Setup a single route by creating its filter chain.\n * This is called lazily by createHandler() and getRouteService().\n *\n * Creates a Service that wraps the filter chain and controller invocation.\n * The service is DTO-only and has no Express dependency.\n *\n * @param key - Route key in format \"METHOD:path\"\n * @param routeWithMeta - Route handler with metadata\n * @returns The service for this route\n */\n public createRouteHandler(\n routeWithMeta: RouteHandlerWithMeta,\n ): Service<MethodMeta, WpResponse<unknown>> {\n const route = routeWithMeta.definition;\n const routeMeta = route.routeMeta;\n\n log.info(`Setting up route: ${routeMeta.httpMethod} ${routeMeta.path}`);\n\n // ONE chain for both HTTP and in-process — no transport tier. The fixed framework\n // filters (LogApiFilter, AuthFilter) are auto-installed and read the transport-neutral\n // HttpRequest, so they run identically in both.\n const filterDefinitions = this.getFilterDefinitions();\n\n // Find matching filters for this route\n const matchingFilters = FilterMatcher.findMatchingFilters(\n route.controllerFilepath,\n filterDefinitions,\n );\n\n // Create service that wraps the controller execution\n const controllerService: Service<MethodMeta, WpResponse<unknown>> = {\n invoke: async (meta: MethodMeta): Promise<WpResponse<unknown>> => {\n const result = await routeWithMeta.invokeControllerHandler.execute(meta);\n // A void endpoint (e.g. a @PubSub cloud-task handler returning Promise<void>)\n // yields undefined; coerce to {} so the response is a non-null JSON body\n // (downstream LogApiCall/serialization require one), mirroring `result ?? {}`.\n return new WpResponse(result ?? {});\n },\n };\n\n if (matchingFilters.length === 0) {\n throw new Error(\n 'No filters found for route — the framework auto-installs LogApiFilter + AuthFilter, so this indicates a wiring problem.',\n );\n }\n\n // Chain filters: highest priority (first in array) should run first (be outermost)\n // Build from innermost (lowest priority) to outermost (highest priority)\n // Start with controller, then wrap with filters in reverse priority order\n let service: Service<MethodMeta, WpResponse<unknown>> = controllerService;\n for (let i = matchingFilters.length - 1; i >= 0; i--) {\n service = matchingFilters[i].chainService(service);\n }\n\n return service;\n }\n\n /**\n * Create an invoker function for a route (for testing via createApiClient).\n * Uses routeMap for O(1) lookup, sets up the filter chain ONCE,\n * and returns a Service that can be called multiple times without\n * recreating the filter chain.\n *\n * This method is called by WebpiecesServer.createApiClient() during proxy setup.\n * The returned Service is stored as the proxy method and invoked on each call.\n *\n * @param method - HTTP method (GET, POST, etc.)\n * @param path - URL path\n * @returns A Service that invokes the route\n */\n createRouteInvoker(method: string, path: string): Service<MethodMeta, WpResponse<unknown>> {\n // Use routeMap for O(1) lookup (not linear search!)\n const key = this.createRouteKey(method, path);\n const routeWithMeta = this.routeMap.get(key);\n\n if (!routeWithMeta) {\n throw new Error(`Route not found: ${method} ${path}`);\n }\n\n // Setup filter chain ONCE (not on every invocation!). Same chain as HTTP — auth included.\n return this.createRouteHandler(routeWithMeta);\n }\n\n /**\n * Look up the RouteMetadata (incl. authMeta) for a registered route by method+path.\n * Used to build a MethodMeta for an in-process dispatch (e.g. a delivered cloud\n * task) so the filter chain sees the same routeMeta production HTTP would.\n *\n * @returns the route's RouteMetadata, or undefined if no route is registered.\n */\n getRouteMeta(method: string, path: string): RouteMetadata | undefined {\n const key = this.createRouteKey(method, path);\n return this.routeMap.get(key)?.definition.routeMeta;\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { AuthorizationService } from './AuthorizationHook';
1
2
  import { InvocationAuthentication } from './InvocationAuthentication';
2
3
  import { Container, ContainerModule } from 'inversify';
3
4
  import { RouteBuilderImpl } from './RouteBuilderImpl';
@@ -97,6 +98,7 @@ export declare class WebpiecesRouter implements ApiFactory {
97
98
  * internal RouteBuilder never leaves.
98
99
  */
99
100
  apiClients(): ApiClient[];
101
+ authorizationService(): AuthorizationService;
100
102
  /** The application DI container (child of the framework container). */
101
103
  getContainer(): Container;
102
104
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WebpiecesRouterFactory = exports.WebpiecesRouter = void 0;
4
4
  const tslib_1 = require("tslib");
5
+ const AuthorizationHook_1 = require("./AuthorizationHook");
5
6
  const inversify_1 = require("inversify");
6
7
  const binding_decorators_1 = require("@inversifyjs/binding-decorators");
7
8
  const core_util_1 = require("@webpieces/core-util");
@@ -13,6 +14,7 @@ const WebpiecesConfig_1 = require("./WebpiecesConfig");
13
14
  const ApiClientFactory_1 = require("./ApiClientFactory");
14
15
  const LogApiFilter_1 = require("./filters/LogApiFilter");
15
16
  const AuthFilter_1 = require("./filters/AuthFilter");
17
+ const AuthorizationFilter_1 = require("./filters/AuthorizationFilter");
16
18
  /**
17
19
  * WebpiecesRouter - the node-only heart of a webpieces app: a DI container + a filter
18
20
  * chain + an in-process API client. It has NO express dependency, so it runs anywhere
@@ -80,6 +82,7 @@ let WebpiecesRouter = class WebpiecesRouter {
80
82
  installFixedFilters() {
81
83
  this.addFilter(new WebAppMeta_1.FilterDefinition(1_000_000, LogApiFilter_1.LogApiFilter, '*'));
82
84
  this.addFilter(new WebAppMeta_1.FilterDefinition(900_000, AuthFilter_1.AuthFilter, '*'));
85
+ this.addFilter(new WebAppMeta_1.FilterDefinition(800_000, AuthorizationFilter_1.AuthorizationFilter, '*'));
83
86
  }
84
87
  async loadDIModules(options) {
85
88
  // Load BOTH registries: framework classes (provideFrameworkSingleton) + the client's
@@ -134,6 +137,9 @@ let WebpiecesRouter = class WebpiecesRouter {
134
137
  apiClients() {
135
138
  return this.apiClientFactory.apiClients();
136
139
  }
140
+ authorizationService() {
141
+ return this.appContainer.get(AuthorizationHook_1.AuthorizationService);
142
+ }
137
143
  /** The application DI container (child of the framework container). */
138
144
  getContainer() {
139
145
  return this.appContainer;
@@ -1 +1 @@
1
- {"version":3,"file":"WebpiecesRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/WebpiecesRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { InvocationAuthentication } from './InvocationAuthentication';\nimport { Container, ContainerModule, inject } from 'inversify';\nimport { buildProviderModule } from '@inversifyjs/binding-decorators';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { provideFrameworkSingleton, buildFrameworkModule } from '@webpieces/core-context';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\nimport { FilterDefinition } from './WebAppMeta';\nimport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\nimport { ApiClientFactory } from './ApiClientFactory';\nimport { ApiFactory } from './ApiFactory';\nimport { ApiClient } from './ApiClient';\nimport { LogApiFilter } from './filters/LogApiFilter';\nimport { AuthFilter } from './filters/AuthFilter';\n\n/**\n * Options for {@link WebpiecesRouterFactory.create} — one object (config lives inside it).\n *\n * appBindings - DI ContainerModules to load (framework + app). Loaded after the\n * @provideSingleton auto-scan so they can add/override bindings.\n * appOverrides - A single ContainerModule loaded LAST so tests can rebind real\n * controllers/clients to mocks (see @webpieces/core-mock createMock()).\n * config - Optional {@link WebpiecesConfig} (recording flags, etc.); defaults to a fresh one.\n */\nexport interface WebpiecesRouterOptions {\n appBindings: ContainerModule[];\n appOverrides?: ContainerModule;\n config?: WebpiecesConfig;\n}\n\n/**\n * WebpiecesRouter - the node-only heart of a webpieces app: a DI container + a filter\n * chain + an in-process API client. It has NO express dependency, so it runs anywhere\n * node runs and is fully testable with zero HTTP.\n *\n * DI-resolved from the platform container (like the old WebpiecesServerImpl):\n * `@provideSingleton @injectable`, RouteBuilderImpl injected, and the two containers set in\n * initialize(). Built by {@link WebpiecesRouterFactory.create} — never `new`ed by callers.\n *\n * Two-container pattern (mirrors Java WebPieces):\n * - webpiecesContainer : framework bindings (config token, @DocumentDesign design roots)\n * - appContainer : your controllers/filters/modules (a child of the framework one)\n *\n * Usage:\n * ```typescript\n * const router = await WebpiecesRouterFactory.create({ appBindings: [AppModule] });\n * router.addRoutes(SaveApi, SaveController);\n * router.addFilter(new FilterDefinition(1800, MyFilter, '*')); // your own filters\n * // (LogApiFilter + AuthFilter are auto-installed above yours; auth is AuthMode-driven.\n * // LogApiFilter logs request+response for EVERY call — do NOT install it yourself.)\n *\n * // test (no express): runs the SAME filter chain (incl. auth) -> controller\n * const api = router.createApiClient(SaveApi);\n * await api.save(new SaveRequest(...));\n * ```\n *\n * To serve real HTTP, hand this router to the express adapter in @webpieces/http-server\n * (bindExpress / bindAndStartExpress) — express lifecycle lives THERE, never here.\n *\n * @DocumentDesign marks it a design root so it appears in http-routing's designed-lib graph.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\nexport class WebpiecesRouter implements ApiFactory {\n private webpiecesContainer!: Container;\n private appContainer!: Container;\n\n constructor(\n @inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl,\n @inject(ApiClientFactory) private readonly apiClientFactory: ApiClientFactory,\n ) {}\n\n /**\n * Build the app container (child of the framework container), load the @provideSingleton\n * auto-scan + appBindings + appOverrides, and point the RouteBuilder at it. Called once by\n * the factory after this router is resolved from the framework container.\n */\n async initialize(\n webpiecesContainer: Container,\n options: WebpiecesRouterOptions,\n ): Promise<void> {\n this.webpiecesContainer = webpiecesContainer;\n\n // App container is a child so app bindings see framework bindings while staying separate.\n // autobind:true lets a concrete @injectable(Singleton) app class self-bind on first resolve\n // (inject-by-type, no @provideSingleton). Framework singletons stay explicit in the parent\n // (provideFrameworkSingleton -> buildFrameworkModule), found before child-autobind kicks in.\n this.appContainer = new Container({ parent: webpiecesContainer, autobind: true });\n this.routeBuilder.setContainer(this.appContainer);\n\n await this.loadDIModules(options);\n this.installFixedFilters();\n }\n\n /**\n * Auto-install the two fixed framework filters on every route (apps add only their own\n * filters below these): LogApiFilter outermost (logs request + response/failure for every\n * call and stamps [Controller.method], then re-throws for the transport to translate), then\n * AuthFilter (enforces the endpoint's AuthMode off the HttpRequest). Both run over HTTP AND\n * in-process — there is no transport tier. Because LogApiFilter is outermost, requests that\n * AuthFilter rejects (401) are still logged with their body + controller identity.\n */\n private installFixedFilters(): void {\n this.addFilter(new FilterDefinition(1_000_000, LogApiFilter, '*'));\n this.addFilter(new FilterDefinition(900_000, AuthFilter, '*'));\n }\n\n private async loadDIModules(options: WebpiecesRouterOptions): Promise<void> {\n // Load BOTH registries: framework classes (provideFrameworkSingleton) + the client's\n // own @provideSingleton classes (binding-decorators global). A client's\n // buildProviderModule() only ever contains the client's classes — never framework internals.\n await this.appContainer.load(buildFrameworkModule());\n await this.appContainer.load(buildProviderModule());\n\n // Load all app modules into application container\n // (webpiecesContainer is currently empty, reserved for future framework bindings)\n for (const module of options.appBindings) {\n await this.appContainer.load(module);\n }\n\n // Load appOverrides LAST so they can override existing bindings\n if (options.appOverrides) {\n await this.appContainer.load(options.appOverrides);\n }\n }\n\n /**\n * Wire an API prototype (with @ApiPath/@Endpoint decorators) to its controller.\n * The controller is resolved from the container at request time.\n */\n addRoutes<TApi, TController extends TApi>(\n api: ClassType<TApi>,\n controller: ClassType<TController>,\n ): this {\n new ApiRoutingFactory(api, controller).configure(this.routeBuilder);\n return this;\n }\n\n /**\n * Register a user filter (runs in-process AND over HTTP, below the auto-installed fixed\n * LogApiFilter + AuthFilter).\n */\n addFilter(filter: FilterDefinition): this {\n this.routeBuilder.addFilter(filter);\n return this;\n }\n\n /**\n * Create an in-process API client that runs the api-tier filter chain + controller\n * with NO express/HTTP. The primary path for tests and node-only callers.\n */\n // webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args\n createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T {\n return this.apiClientFactory.createApiClient(apiPrototype);\n }\n\n /** Transport-only credential bridge; ordinary endpoint filters remain mandatory. */\n // webpieces-disable no-any-unknown -- abstract API constructor signature\n createInvocationApiClient<T>(\n apiPrototype: abstract new (...args: any[]) => T,\n authentication: InvocationAuthentication,\n ): T {\n return this.apiClientFactory.createInvocationApiClient(apiPrototype, authentication);\n }\n\n /**\n * Reify the registered APIs as {@link ApiClient}s (contract + the createApiClient proxy) via\n * the shared {@link ApiClientFactory}. This is the ONLY handoff to the express layer — the\n * internal RouteBuilder never leaves.\n */\n apiClients(): ApiClient[] {\n return this.apiClientFactory.apiClients();\n }\n\n /** The application DI container (child of the framework container). */\n getContainer(): Container {\n return this.appContainer;\n }\n}\n\n/**\n * Builds a {@link WebpiecesRouter}: constructs the platform container (mirrors\n * WebpiecesServerFactory.create), RESOLVES the router from DI, then initializes its app child\n * container with the @provideSingleton auto-scan + appBindings + optional test overrides.\n */\nexport class WebpiecesRouterFactory {\n static async create(options: WebpiecesRouterOptions): Promise<WebpiecesRouter> {\n // Platform (framework) container — build via buildFrameworkModule so framework\n // singletons (WebpiecesRouter, RouteBuilderImpl) come from the webpieces registry,\n // NOT the client's global one.\n const webpiecesContainer = new Container();\n webpiecesContainer\n .bind(WEBPIECES_CONFIG_TOKEN)\n .toConstantValue(options.config ?? new WebpiecesConfig());\n await webpiecesContainer.load(buildFrameworkModule());\n\n // Resolve the router from the container (NOT new'd) so @DocumentDesign + DI hold.\n const router = webpiecesContainer.get(WebpiecesRouter);\n await router.initialize(webpiecesContainer, options);\n return router;\n }\n}\n"]}
1
+ {"version":3,"file":"WebpiecesRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/WebpiecesRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { AuthorizationService } from './AuthorizationHook';\nimport { InvocationAuthentication } from './InvocationAuthentication';\nimport { Container, ContainerModule, inject } from 'inversify';\nimport { buildProviderModule } from '@inversifyjs/binding-decorators';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { provideFrameworkSingleton, buildFrameworkModule } from '@webpieces/core-context';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\nimport { FilterDefinition } from './WebAppMeta';\nimport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\nimport { ApiClientFactory } from './ApiClientFactory';\nimport { ApiFactory } from './ApiFactory';\nimport { ApiClient } from './ApiClient';\nimport { LogApiFilter } from './filters/LogApiFilter';\nimport { AuthFilter } from './filters/AuthFilter';\nimport { AuthorizationFilter } from './filters/AuthorizationFilter';\n\n/**\n * Options for {@link WebpiecesRouterFactory.create} — one object (config lives inside it).\n *\n * appBindings - DI ContainerModules to load (framework + app). Loaded after the\n * @provideSingleton auto-scan so they can add/override bindings.\n * appOverrides - A single ContainerModule loaded LAST so tests can rebind real\n * controllers/clients to mocks (see @webpieces/core-mock createMock()).\n * config - Optional {@link WebpiecesConfig} (recording flags, etc.); defaults to a fresh one.\n */\nexport interface WebpiecesRouterOptions {\n appBindings: ContainerModule[];\n appOverrides?: ContainerModule;\n config?: WebpiecesConfig;\n}\n\n/**\n * WebpiecesRouter - the node-only heart of a webpieces app: a DI container + a filter\n * chain + an in-process API client. It has NO express dependency, so it runs anywhere\n * node runs and is fully testable with zero HTTP.\n *\n * DI-resolved from the platform container (like the old WebpiecesServerImpl):\n * `@provideSingleton @injectable`, RouteBuilderImpl injected, and the two containers set in\n * initialize(). Built by {@link WebpiecesRouterFactory.create} — never `new`ed by callers.\n *\n * Two-container pattern (mirrors Java WebPieces):\n * - webpiecesContainer : framework bindings (config token, @DocumentDesign design roots)\n * - appContainer : your controllers/filters/modules (a child of the framework one)\n *\n * Usage:\n * ```typescript\n * const router = await WebpiecesRouterFactory.create({ appBindings: [AppModule] });\n * router.addRoutes(SaveApi, SaveController);\n * router.addFilter(new FilterDefinition(1800, MyFilter, '*')); // your own filters\n * // (LogApiFilter + AuthFilter are auto-installed above yours; auth is AuthMode-driven.\n * // LogApiFilter logs request+response for EVERY call — do NOT install it yourself.)\n *\n * // test (no express): runs the SAME filter chain (incl. auth) -> controller\n * const api = router.createApiClient(SaveApi);\n * await api.save(new SaveRequest(...));\n * ```\n *\n * To serve real HTTP, hand this router to the express adapter in @webpieces/http-server\n * (bindExpress / bindAndStartExpress) — express lifecycle lives THERE, never here.\n *\n * @DocumentDesign marks it a design root so it appears in http-routing's designed-lib graph.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\nexport class WebpiecesRouter implements ApiFactory {\n private webpiecesContainer!: Container;\n private appContainer!: Container;\n\n constructor(\n @inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl,\n @inject(ApiClientFactory) private readonly apiClientFactory: ApiClientFactory,\n ) {}\n\n /**\n * Build the app container (child of the framework container), load the @provideSingleton\n * auto-scan + appBindings + appOverrides, and point the RouteBuilder at it. Called once by\n * the factory after this router is resolved from the framework container.\n */\n async initialize(\n webpiecesContainer: Container,\n options: WebpiecesRouterOptions,\n ): Promise<void> {\n this.webpiecesContainer = webpiecesContainer;\n\n // App container is a child so app bindings see framework bindings while staying separate.\n // autobind:true lets a concrete @injectable(Singleton) app class self-bind on first resolve\n // (inject-by-type, no @provideSingleton). Framework singletons stay explicit in the parent\n // (provideFrameworkSingleton -> buildFrameworkModule), found before child-autobind kicks in.\n this.appContainer = new Container({ parent: webpiecesContainer, autobind: true });\n this.routeBuilder.setContainer(this.appContainer);\n\n await this.loadDIModules(options);\n this.installFixedFilters();\n }\n\n /**\n * Auto-install the two fixed framework filters on every route (apps add only their own\n * filters below these): LogApiFilter outermost (logs request + response/failure for every\n * call and stamps [Controller.method], then re-throws for the transport to translate), then\n * AuthFilter (enforces the endpoint's AuthMode off the HttpRequest). Both run over HTTP AND\n * in-process — there is no transport tier. Because LogApiFilter is outermost, requests that\n * AuthFilter rejects (401) are still logged with their body + controller identity.\n */\n private installFixedFilters(): void {\n this.addFilter(new FilterDefinition(1_000_000, LogApiFilter, '*'));\n this.addFilter(new FilterDefinition(900_000, AuthFilter, '*'));\n this.addFilter(new FilterDefinition(800_000, AuthorizationFilter, '*'));\n }\n\n private async loadDIModules(options: WebpiecesRouterOptions): Promise<void> {\n // Load BOTH registries: framework classes (provideFrameworkSingleton) + the client's\n // own @provideSingleton classes (binding-decorators global). A client's\n // buildProviderModule() only ever contains the client's classes — never framework internals.\n await this.appContainer.load(buildFrameworkModule());\n await this.appContainer.load(buildProviderModule());\n\n // Load all app modules into application container\n // (webpiecesContainer is currently empty, reserved for future framework bindings)\n for (const module of options.appBindings) {\n await this.appContainer.load(module);\n }\n\n // Load appOverrides LAST so they can override existing bindings\n if (options.appOverrides) {\n await this.appContainer.load(options.appOverrides);\n }\n }\n\n /**\n * Wire an API prototype (with @ApiPath/@Endpoint decorators) to its controller.\n * The controller is resolved from the container at request time.\n */\n addRoutes<TApi, TController extends TApi>(\n api: ClassType<TApi>,\n controller: ClassType<TController>,\n ): this {\n new ApiRoutingFactory(api, controller).configure(this.routeBuilder);\n return this;\n }\n\n /**\n * Register a user filter (runs in-process AND over HTTP, below the auto-installed fixed\n * LogApiFilter + AuthFilter).\n */\n addFilter(filter: FilterDefinition): this {\n this.routeBuilder.addFilter(filter);\n return this;\n }\n\n /**\n * Create an in-process API client that runs the api-tier filter chain + controller\n * with NO express/HTTP. The primary path for tests and node-only callers.\n */\n // webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args\n createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T {\n return this.apiClientFactory.createApiClient(apiPrototype);\n }\n\n /** Transport-only credential bridge; ordinary endpoint filters remain mandatory. */\n // webpieces-disable no-any-unknown -- abstract API constructor signature\n createInvocationApiClient<T>(\n apiPrototype: abstract new (...args: any[]) => T,\n authentication: InvocationAuthentication,\n ): T {\n return this.apiClientFactory.createInvocationApiClient(apiPrototype, authentication);\n }\n\n /**\n * Reify the registered APIs as {@link ApiClient}s (contract + the createApiClient proxy) via\n * the shared {@link ApiClientFactory}. This is the ONLY handoff to the express layer — the\n * internal RouteBuilder never leaves.\n */\n apiClients(): ApiClient[] {\n return this.apiClientFactory.apiClients();\n }\n\n authorizationService(): AuthorizationService {\n return this.appContainer.get(AuthorizationService);\n }\n\n /** The application DI container (child of the framework container). */\n getContainer(): Container {\n return this.appContainer;\n }\n}\n\n/**\n * Builds a {@link WebpiecesRouter}: constructs the platform container (mirrors\n * WebpiecesServerFactory.create), RESOLVES the router from DI, then initializes its app child\n * container with the @provideSingleton auto-scan + appBindings + optional test overrides.\n */\nexport class WebpiecesRouterFactory {\n static async create(options: WebpiecesRouterOptions): Promise<WebpiecesRouter> {\n // Platform (framework) container — build via buildFrameworkModule so framework\n // singletons (WebpiecesRouter, RouteBuilderImpl) come from the webpieces registry,\n // NOT the client's global one.\n const webpiecesContainer = new Container();\n webpiecesContainer\n .bind(WEBPIECES_CONFIG_TOKEN)\n .toConstantValue(options.config ?? new WebpiecesConfig());\n await webpiecesContainer.load(buildFrameworkModule());\n\n // Resolve the router from the container (NOT new'd) so @DocumentDesign + DI hold.\n const router = webpiecesContainer.get(WebpiecesRouter);\n await router.initialize(webpiecesContainer, options);\n return router;\n }\n}\n"]}