@webpieces/http-routing 0.4.922 → 0.4.923

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/http-routing",
3
- "version": "0.4.922",
3
+ "version": "0.4.923",
4
4
  "description": "Decorator-based routing with auto-wiring for WebPieces",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -22,14 +22,14 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@inversifyjs/binding-decorators": "1.1.5",
25
- "@webpieces/core-context": "0.4.922",
26
- "@webpieces/core-util": "0.4.922",
27
- "@webpieces/gcp-identity": "0.4.922",
25
+ "@webpieces/core-context": "0.4.923",
26
+ "@webpieces/core-util": "0.4.923",
27
+ "@webpieces/gcp-identity": "0.4.923",
28
28
  "inversify": "7.10.4",
29
29
  "jsonwebtoken": "9.0.2",
30
30
  "minimatch": "10.0.1",
31
- "@webpieces/http-client-core": "0.4.922",
32
- "@webpieces/http-client-node": "0.4.922",
33
- "@webpieces/cloudtasks-client": "0.4.922"
31
+ "@webpieces/http-client-core": "0.4.923",
32
+ "@webpieces/http-client-node": "0.4.923",
33
+ "@webpieces/cloudtasks-client": "0.4.923"
34
34
  }
35
35
  }
@@ -11,6 +11,9 @@ export declare class AuthorizedApiDocument {
11
11
  private readonly methods;
12
12
  constructor(policy: AuthorizationService, routes: readonly RouteMetadata[]);
13
13
  project(catalog: ApiDocumentObject): Promise<ApiDocumentObject>;
14
+ /** A route path as the document spells it: `:id` becomes `{id}`. */
15
+ private template;
16
+ private operationKey;
14
17
  private permits;
15
18
  private pruneComponents;
16
19
  private references;
@@ -14,7 +14,10 @@ class AuthorizedApiDocument {
14
14
  if (!route.authorization || !route.apiClass)
15
15
  throw new core_util_1.ApiImplementationError('Document routes require canonical receiving endpoint metadata.');
16
16
  policy.validate(route.authorization);
17
- const id = `${route.apiClass.name}_${route.methodName}`;
17
+ // Keyed by the HTTP method and path template, never by operationId: a manifest chooses how
18
+ // operationIds are spelled (`contract_method` or bare `method`, #1270), and the method+path
19
+ // pair is the one identity every spelling shares with the receiving route.
20
+ const id = this.operationKey(route.httpMethod.toLowerCase(), this.template(route.path));
18
21
  if (this.routes.has(id))
19
22
  throw new core_util_1.ApiImplementationError(`Duplicate document operation ${id}.`);
20
23
  this.routes.set(id, route);
@@ -32,10 +35,10 @@ class AuthorizedApiDocument {
32
35
  if (!this.methods.includes(method))
33
36
  continue;
34
37
  const operation = this.object(item[method]);
35
- const id = operation['operationId'];
36
- const route = typeof id === 'string' ? this.routes.get(id) : undefined;
37
- if (!route?.authorization || route.path.replace(/:([A-Za-z_]\w*)/g, '{$1}') !== path || route.httpMethod.toLowerCase() !== method)
38
- throw new core_util_1.ApiImplementationError(`Document operation ${String(id)} has no matching receiving endpoint policy.`);
38
+ const id = this.operationKey(method, path);
39
+ const route = this.routes.get(id);
40
+ if (!route?.authorization)
41
+ throw new core_util_1.ApiImplementationError(`Document operation ${id} (operationId ${String(operation['operationId'])}) has no matching receiving endpoint policy.`);
39
42
  if (!(await this.permits(route))) {
40
43
  delete item[method];
41
44
  continue;
@@ -58,6 +61,13 @@ class AuthorizedApiDocument {
58
61
  this.pruneComponents(document);
59
62
  return document;
60
63
  }
64
+ /** A route path as the document spells it: `:id` becomes `{id}`. */
65
+ template(routePath) {
66
+ return routePath.replace(/:([A-Za-z_]\w*)/g, '{$1}');
67
+ }
68
+ operationKey(method, documentPath) {
69
+ return `${method.toUpperCase()} ${documentPath}`;
70
+ }
61
71
  async permits(route) {
62
72
  if (!route.authorization)
63
73
  throw new core_util_1.ApiImplementationError('Missing document authorization.');
@@ -1 +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"]}
1
+ {"version":3,"file":"AuthorizedApiDocument.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthorizedApiDocument.ts"],"names":[],"mappings":"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","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 // Keyed by the HTTP method and path template, never by operationId: a manifest chooses how\n // operationIds are spelled (`contract_method` or bare `method`, #1270), and the method+path\n // pair is the one identity every spelling shares with the receiving route.\n const id = this.operationKey(route.httpMethod.toLowerCase(), this.template(route.path));\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 = this.operationKey(method, path);\n const route = this.routes.get(id);\n if (!route?.authorization) throw new ApiImplementationError(`Document operation ${id} (operationId ${String(operation['operationId'])}) 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 /** A route path as the document spells it: `:id` becomes `{id}`. */\n private template(routePath: string): string {\n return routePath.replace(/:([A-Za-z_]\\w*)/g, '{$1}');\n }\n\n private operationKey(method: string, documentPath: string): string {\n return `${method.toUpperCase()} ${documentPath}`;\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"]}