@lucaapp/service-utils 5.29.0 → 5.29.1

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.
@@ -225,6 +225,27 @@ const hoistCombinatorRequiredFields = (spec) => {
225
225
  visit(schemas);
226
226
  };
227
227
  exports.hoistCombinatorRequiredFields = hoistCombinatorRequiredFields;
228
+ /**
229
+ * Turn each middleware's declared schemes into OpenAPI security requirements.
230
+ *
231
+ * OpenAPI expresses the two relations differently: separate objects in the
232
+ * `security` array are alternatives (OR), while several schemes inside one
233
+ * object are all required (AND). A middleware's own `security: [a, b]` means
234
+ * "a or b" -- it accepts either credential -- but two middlewares on the same
235
+ * route both run, so their requirements are combined with AND.
236
+ *
237
+ * That makes the result the cartesian product across middlewares:
238
+ *
239
+ * [[a, b], [c]] -> [{a, c}, {b, c}]
240
+ *
241
+ * A single middleware therefore still renders as plain alternatives, which is
242
+ * what it did before, and stacked middlewares no longer render as if presenting
243
+ * any one of their credentials were enough.
244
+ */
245
+ const buildSecurityRequirements = (securityByMiddleware) => securityByMiddleware.reduce((requirements, alternatives) => requirements.flatMap(requirement =>
246
+ // Duplicates collapse on their own: the same scheme required twice is
247
+ // still one entry in the object.
248
+ alternatives.map(scheme => ({ ...requirement, [scheme]: [] }))), [{}]);
228
249
  const registerEndpoint = (registry, method, prefixPath, rootPrefixPath, path, summary,
229
250
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
230
251
  options, tags, debug) => {
@@ -233,7 +254,11 @@ options, tags, debug) => {
233
254
  let querySchema = options.schemas?.query || undefined;
234
255
  let headersSchema = options.schemas?.headers || undefined;
235
256
  let responseSchemas = options.responses;
236
- const securitySchemes = new Set();
257
+ // One entry per security-declaring middleware, each holding that middleware's
258
+ // alternatives. Kept separate because the two levels mean different things:
259
+ // within a middleware the schemes are alternatives, across middlewares they
260
+ // are all enforced. See buildSecurityRequirements.
261
+ const securityByMiddleware = [];
237
262
  if (options.errors) {
238
263
  for (const errorType of Object.keys(options.errors)) {
239
264
  const statusCode = options.errors[errorType];
@@ -294,8 +319,9 @@ options, tags, debug) => {
294
319
  });
295
320
  }
296
321
  }
297
- for (const scheme of middleware.options.security || []) {
298
- securitySchemes.add(scheme);
322
+ const middlewareSecurity = middleware.options.security || [];
323
+ if (middlewareSecurity.length > 0) {
324
+ securityByMiddleware.push([...middlewareSecurity]);
299
325
  }
300
326
  }
301
327
  // Add ZodError response
@@ -355,8 +381,8 @@ options, tags, debug) => {
355
381
  responses: responseMap,
356
382
  operationId: options.operationId ||
357
383
  deriveOperationId(registry, method, prefixPath.slice(rootPrefixPath.length) + path),
358
- ...(securitySchemes.size > 0 && {
359
- security: [...securitySchemes].map(scheme => ({ [scheme]: [] })),
384
+ ...(securityByMiddleware.length > 0 && {
385
+ security: buildSecurityRequirements(securityByMiddleware),
360
386
  }),
361
387
  });
362
388
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucaapp/service-utils",
3
- "version": "5.29.0",
3
+ "version": "5.29.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [