@lucaapp/service-utils 5.26.0 → 5.29.0

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.
@@ -28,6 +28,7 @@ export declare class Api {
28
28
  apiTitle: string;
29
29
  apiDescription: string;
30
30
  private prefixPath;
31
+ private rootPrefixPath;
31
32
  debug: boolean;
32
33
  tags: string[];
33
34
  constructor(options?: ApiConstructorOptions);
@@ -10,6 +10,7 @@ const swagger_ui_express_1 = __importDefault(require("swagger-ui-express"));
10
10
  const zod_to_openapi_1 = require("@asteasolutions/zod-to-openapi");
11
11
  const endpoint_1 = require("./endpoint");
12
12
  const openapi_1 = require("./openapi");
13
+ const security_1 = require("./security");
13
14
  const body_parser_1 = __importDefault(require("body-parser"));
14
15
  const rawBodySaver = function (req, res, buf, encoding) {
15
16
  if (buf && buf.length) {
@@ -22,6 +23,7 @@ class Api {
22
23
  this.router = options.router || (0, express_1.Router)();
23
24
  this.registry = options.registry || new zod_to_openapi_1.OpenAPIRegistry();
24
25
  this.prefixPath = options.prefixPath || '';
26
+ this.rootPrefixPath = this.prefixPath;
25
27
  this.openApiVersion = options.openApiVersion || '3.0.0';
26
28
  this.apiVersion = options.apiVersion || '1.0.0';
27
29
  this.apiTitle = options.apiTitle || 'API';
@@ -31,33 +33,35 @@ class Api {
31
33
  this.router.use(body_parser_1.default.json({ verify: rawBodySaver, limit: '2mb' }));
32
34
  }
33
35
  child(options) {
34
- return new Api({
36
+ const child = new Api({
35
37
  prefixPath: this.prefixPath + (options.prefixPath || ''),
36
38
  debug: this.debug,
37
39
  tags: [...this.tags, ...(options.tags || [])],
38
40
  registry: this.registry,
39
41
  router: this.router,
40
42
  });
43
+ child.rootPrefixPath = this.rootPrefixPath;
44
+ return child;
41
45
  }
42
46
  get(path, summary, options, handler) {
43
47
  (0, endpoint_1.mountEndpoint)(this.router, 'get', path, options, handler, this.debug);
44
- (0, openapi_1.registerEndpoint)(this.registry, 'get', this.prefixPath, path, summary, options, this.tags, this.debug);
48
+ (0, openapi_1.registerEndpoint)(this.registry, 'get', this.prefixPath, this.rootPrefixPath, path, summary, options, this.tags, this.debug);
45
49
  }
46
50
  post(path, summary, options, handler) {
47
51
  (0, endpoint_1.mountEndpoint)(this.router, 'post', path, options, handler, this.debug);
48
- (0, openapi_1.registerEndpoint)(this.registry, 'post', this.prefixPath, path, summary, options, this.tags, this.debug);
52
+ (0, openapi_1.registerEndpoint)(this.registry, 'post', this.prefixPath, this.rootPrefixPath, path, summary, options, this.tags, this.debug);
49
53
  }
50
54
  patch(path, summary, options, handler) {
51
55
  (0, endpoint_1.mountEndpoint)(this.router, 'patch', path, options, handler, this.debug);
52
- (0, openapi_1.registerEndpoint)(this.registry, 'patch', this.prefixPath, path, summary, options, this.tags, this.debug);
56
+ (0, openapi_1.registerEndpoint)(this.registry, 'patch', this.prefixPath, this.rootPrefixPath, path, summary, options, this.tags, this.debug);
53
57
  }
54
58
  put(path, summary, options, handler) {
55
59
  (0, endpoint_1.mountEndpoint)(this.router, 'put', path, options, handler, this.debug);
56
- (0, openapi_1.registerEndpoint)(this.registry, 'put', this.prefixPath, path, summary, options, this.tags, this.debug);
60
+ (0, openapi_1.registerEndpoint)(this.registry, 'put', this.prefixPath, this.rootPrefixPath, path, summary, options, this.tags, this.debug);
57
61
  }
58
62
  delete(path, summary, options, handler) {
59
63
  (0, endpoint_1.mountEndpoint)(this.router, 'delete', path, options, handler, this.debug);
60
- (0, openapi_1.registerEndpoint)(this.registry, 'delete', this.prefixPath, path, summary, options, this.tags, this.debug);
64
+ (0, openapi_1.registerEndpoint)(this.registry, 'delete', this.prefixPath, this.rootPrefixPath, path, summary, options, this.tags, this.debug);
61
65
  }
62
66
  generateOpenAPISpec() {
63
67
  let generator;
@@ -74,9 +78,11 @@ class Api {
74
78
  title: this.apiTitle,
75
79
  description: this.apiDescription,
76
80
  },
81
+ servers: [{ url: '/' }],
77
82
  });
78
83
  (0, openapi_1.forcePathParamsRequired)(spec);
79
84
  (0, openapi_1.hoistCombinatorRequiredFields)(spec);
85
+ spec.components = { ...spec.components, securitySchemes: security_1.SECURITY_SCHEMES };
80
86
  return spec;
81
87
  }
82
88
  mountSwaggerMiddlewares() {
@@ -3,5 +3,6 @@ export * from './api';
3
3
  export * from './endpoint';
4
4
  export * from './middleware';
5
5
  export * from './response';
6
+ export type { SecurityScheme } from './security';
6
7
  export * from './send';
7
8
  export { hoistCombinatorRequiredFields } from './openapi';
@@ -25,5 +25,5 @@ export declare const hoistCombinatorRequiredFields: (spec: {
25
25
  schemas?: Record<string, unknown>;
26
26
  };
27
27
  }) => void;
28
- export declare const registerEndpoint: (registry: OpenAPIRegistry, method: HttpMethod, prefixPath: string, path: string, summary: string, options: EndpointOptions<any, any, any, any, any, any>, tags: string[], debug: boolean) => void;
28
+ export declare const registerEndpoint: (registry: OpenAPIRegistry, method: HttpMethod, prefixPath: string, rootPrefixPath: string, path: string, summary: string, options: EndpointOptions<any, any, any, any, any, any>, tags: string[], debug: boolean) => void;
29
29
  export {};
@@ -4,6 +4,21 @@ exports.registerEndpoint = exports.hoistCombinatorRequiredFields = exports.force
4
4
  const zod_1 = require("zod");
5
5
  const http_1 = require("./types/http");
6
6
  const mapExpressParamsToOpenAPIParams = (path) => path.replaceAll(/\:([^\:\/]+)/g, '{$1}');
7
+ const deriveOperationId = (registry, method, path) => {
8
+ const operationId = method +
9
+ path
10
+ .replaceAll(':', 'by-')
11
+ .split(/[^A-Za-z0-9]+/)
12
+ .filter(Boolean)
13
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
14
+ .join('');
15
+ const isTaken = registry.definitions.some(definition => definition.type === 'route' &&
16
+ definition.route.operationId === operationId);
17
+ if (isTaken) {
18
+ throw new Error(`Duplicate operationId ${operationId} for ${method.toUpperCase()} ${path}, set options.operationId`);
19
+ }
20
+ return operationId;
21
+ };
7
22
  const buildResponseConfig = (responseOptions) => {
8
23
  const responses = {};
9
24
  responseOptions.forEach(response => {
@@ -210,7 +225,7 @@ const hoistCombinatorRequiredFields = (spec) => {
210
225
  visit(schemas);
211
226
  };
212
227
  exports.hoistCombinatorRequiredFields = hoistCombinatorRequiredFields;
213
- const registerEndpoint = (registry, method, prefixPath, path, summary,
228
+ const registerEndpoint = (registry, method, prefixPath, rootPrefixPath, path, summary,
214
229
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
215
230
  options, tags, debug) => {
216
231
  let bodySchema = options.schemas?.body || undefined;
@@ -218,6 +233,7 @@ options, tags, debug) => {
218
233
  let querySchema = options.schemas?.query || undefined;
219
234
  let headersSchema = options.schemas?.headers || undefined;
220
235
  let responseSchemas = options.responses;
236
+ const securitySchemes = new Set();
221
237
  if (options.errors) {
222
238
  for (const errorType of Object.keys(options.errors)) {
223
239
  const statusCode = options.errors[errorType];
@@ -278,6 +294,9 @@ options, tags, debug) => {
278
294
  });
279
295
  }
280
296
  }
297
+ for (const scheme of middleware.options.security || []) {
298
+ securitySchemes.add(scheme);
299
+ }
281
300
  }
282
301
  // Add ZodError response
283
302
  responseSchemas.push({
@@ -334,7 +353,11 @@ options, tags, debug) => {
334
353
  headers: headersSchema ? [headersSchema] : [],
335
354
  },
336
355
  responses: responseMap,
337
- ...(options.operationId && { operationId: options.operationId }),
356
+ operationId: options.operationId ||
357
+ deriveOperationId(registry, method, prefixPath.slice(rootPrefixPath.length) + path),
358
+ ...(securitySchemes.size > 0 && {
359
+ security: [...securitySchemes].map(scheme => ({ [scheme]: [] })),
360
+ }),
338
361
  });
339
362
  };
340
363
  exports.registerEndpoint = registerEndpoint;
@@ -0,0 +1,3 @@
1
+ import type { SecuritySchemeObject } from 'openapi3-ts/oas30';
2
+ export type SecurityScheme = 'jwt_operator' | 'jwt_support' | 'jwt_operator_device' | 'jwt_consumer' | 'api_key_operator_device' | 'api_key_atworld' | 'api_key_at_support' | 'basic_adyen_webhook' | 'hmac_adyen_webhook';
3
+ export declare const SECURITY_SCHEMES: Record<SecurityScheme, SecuritySchemeObject>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SECURITY_SCHEMES = void 0;
4
+ const xAuth = (description) => ({
5
+ type: 'apiKey',
6
+ in: 'header',
7
+ name: 'X-Auth',
8
+ description,
9
+ });
10
+ const xApiKey = (description) => ({
11
+ type: 'apiKey',
12
+ in: 'header',
13
+ name: 'X-Api-Key',
14
+ description,
15
+ });
16
+ const basicAuth = (description) => ({
17
+ type: 'http',
18
+ scheme: 'basic',
19
+ description,
20
+ });
21
+ const hmacHeader = (description) => ({
22
+ type: 'apiKey',
23
+ in: 'header',
24
+ name: 'hmacsignature',
25
+ description,
26
+ });
27
+ exports.SECURITY_SCHEMES = {
28
+ jwt_operator: xAuth('Operator session JWT issued by the luca backend'),
29
+ jwt_support: xAuth('Support session JWT issued by the luca backend'),
30
+ jwt_operator_device: xAuth('Operator device JWT issued by the luca backend (kiosk, terminal, link)'),
31
+ jwt_consumer: xAuth('Consumer JWT'),
32
+ api_key_operator_device: xApiKey('Operator device API key (sk_...) created by the operator for an external device'),
33
+ api_key_atworld: xApiKey('ATworld partner API key'),
34
+ api_key_at_support: xApiKey('AT support API key (partner or luca)'),
35
+ basic_adyen_webhook: basicAuth('Basic auth credential luca configures on the Adyen webhook endpoint'),
36
+ hmac_adyen_webhook: hmacHeader('Adyen HMAC-SHA256 signature over the raw request body, verified against the configured balance platform HMAC keys'),
37
+ };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { SecurityScheme } from '../security';
2
3
  import { ArrayToUnion, ExtractZodOutput, ZodSchemaOuput } from './utils';
3
4
  export type EndpointResponseSchema = {
4
5
  status: number;
@@ -45,6 +46,7 @@ export type MiddlewareOptions<TResponseSchemas extends ReadonlyArray<EndpointRes
45
46
  returnRawBody?: boolean;
46
47
  };
47
48
  errors?: Record<string, number>;
49
+ security?: ReadonlyArray<SecurityScheme>;
48
50
  };
49
51
  export type Middleware<TResponseSchemas extends ReadonlyArray<EndpointResponseSchema>, TRequestBodySchema, TRequestParamsSchema, TRequestQuerySchema, TRequestHeadersSchema, TContextSchema> = {
50
52
  options: MiddlewareOptions<TResponseSchemas, TRequestBodySchema, TRequestParamsSchema, TRequestQuerySchema, TRequestHeadersSchema, TContextSchema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucaapp/service-utils",
3
- "version": "5.26.0",
3
+ "version": "5.29.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [
@@ -11,7 +11,7 @@
11
11
  "scripts": {
12
12
  "build": "tsc -p tsconfig.build.json",
13
13
  "ts:check": "tsc -p . --noEmit",
14
- "lint": "eslint .",
14
+ "lint": "eslint --quiet .",
15
15
  "test": "vitest run",
16
16
  "test:coverage": "vitest run --coverage",
17
17
  "test:ci": "VITEST_JUNIT_SUITE_NAME=service-utils CI=true vitest run --coverage",
@@ -88,7 +88,7 @@
88
88
  "tar": "7.5.21",
89
89
  "body-parser": "1.20.6",
90
90
  "lodash": "^4.18.1",
91
- "qs": "6.15.2",
91
+ "qs": "6.16.0",
92
92
  "dottie": "2.0.7",
93
93
  "fast-xml-parser": "^5.7.1",
94
94
  "yaml": "^2.8.3",