@lucaapp/service-utils 5.25.0 → 5.27.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.
- package/dist/lib/api/api.d.ts +1 -0
- package/dist/lib/api/api.js +13 -6
- package/dist/lib/api/index.d.ts +2 -0
- package/dist/lib/api/index.js +7 -1
- package/dist/lib/api/openapi.d.ts +19 -1
- package/dist/lib/api/openapi.js +158 -6
- package/dist/lib/api/security.d.ts +3 -0
- package/dist/lib/api/security.js +15 -0
- package/dist/lib/api/types/middleware.d.ts +2 -0
- package/dist/lib/kafka/events/payment.d.ts +13 -1
- package/dist/lib/negotiator/language.d.ts +38 -2
- package/dist/lib/negotiator/language.js +76 -3
- package/package.json +2 -2
package/dist/lib/api/api.d.ts
CHANGED
package/dist/lib/api/api.js
CHANGED
|
@@ -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
|
-
|
|
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,8 +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);
|
|
84
|
+
(0, openapi_1.hoistCombinatorRequiredFields)(spec);
|
|
85
|
+
spec.components = { ...spec.components, securitySchemes: security_1.SECURITY_SCHEMES };
|
|
79
86
|
return spec;
|
|
80
87
|
}
|
|
81
88
|
mountSwaggerMiddlewares() {
|
package/dist/lib/api/index.d.ts
CHANGED
package/dist/lib/api/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.extendZodWithOpenApi = void 0;
|
|
17
|
+
exports.hoistCombinatorRequiredFields = exports.extendZodWithOpenApi = void 0;
|
|
18
18
|
var zod_to_openapi_1 = require("@asteasolutions/zod-to-openapi");
|
|
19
19
|
Object.defineProperty(exports, "extendZodWithOpenApi", { enumerable: true, get: function () { return zod_to_openapi_1.extendZodWithOpenApi; } });
|
|
20
20
|
__exportStar(require("./api"), exports);
|
|
@@ -22,3 +22,9 @@ __exportStar(require("./endpoint"), exports);
|
|
|
22
22
|
__exportStar(require("./middleware"), exports);
|
|
23
23
|
__exportStar(require("./response"), exports);
|
|
24
24
|
__exportStar(require("./send"), exports);
|
|
25
|
+
// Normalizes an OpenAPI spec's anyOf/allOf combinators the same way
|
|
26
|
+
// generateOpenAPISpec() does, so a frozen/historical spec (a POS compat
|
|
27
|
+
// baseline that predates this generator's fix) can be diffed against a
|
|
28
|
+
// current spec on equal terms. See openapi.ts for what "normalize" means.
|
|
29
|
+
var openapi_1 = require("./openapi");
|
|
30
|
+
Object.defineProperty(exports, "hoistCombinatorRequiredFields", { enumerable: true, get: function () { return openapi_1.hoistCombinatorRequiredFields; } });
|
|
@@ -7,5 +7,23 @@ export declare const forcePathParamsRequired: (spec: {
|
|
|
7
7
|
parameters?: Record<string, unknown>;
|
|
8
8
|
};
|
|
9
9
|
}) => void;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* zod-to-openapi represents a same-status multi-error response as `anyOf`, and
|
|
12
|
+
* `.extend()` on a registered/ref'd schema as `allOf: [$ref, inline]`. Either
|
|
13
|
+
* shape hides required/property information that used to be flat at the parent
|
|
14
|
+
* level inside the combinator, which oasdiff's breaking-change diff cannot see
|
|
15
|
+
* through — it reports every such change as fields being removed, even when the
|
|
16
|
+
* change was purely additive. This walks the finished spec and restores the
|
|
17
|
+
* safely-hoistable subset of that information to the parent schema (see
|
|
18
|
+
* hoistAnyOf / hoistAllOf for what "safe" means for each combinator — they are
|
|
19
|
+
* NOT the same rule). Ref topology is untouched: refs are only read to compute
|
|
20
|
+
* what's safe to hoist, never rewritten.
|
|
21
|
+
*/
|
|
22
|
+
export declare const hoistCombinatorRequiredFields: (spec: {
|
|
23
|
+
paths?: Record<string, unknown>;
|
|
24
|
+
components?: {
|
|
25
|
+
schemas?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
}) => 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;
|
|
11
29
|
export {};
|
package/dist/lib/api/openapi.js
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.registerEndpoint = exports.forcePathParamsRequired = void 0;
|
|
3
|
+
exports.registerEndpoint = exports.hoistCombinatorRequiredFields = exports.forcePathParamsRequired = void 0;
|
|
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 => {
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
25
|
+
// Raw responses that declare no real schema (rawResponseEnd()'s default,
|
|
26
|
+
// schema: z.void()) end the request outside the schema layer and carry
|
|
27
|
+
// the placeholder status 0, which is not a valid OpenAPI response key —
|
|
28
|
+
// skip them; the fallback below documents a generic 200 instead. A route
|
|
29
|
+
// that sets isRawResponseEnd but ALSO declares a real schema (bypassing
|
|
30
|
+
// the response() helper for streaming/binary output while still telling
|
|
31
|
+
// callers what the body looks like) must still be documented normally —
|
|
32
|
+
// skipping unconditionally silently drops real, intentional schema
|
|
33
|
+
// information (LUCA-35888).
|
|
34
|
+
if (response.isRawResponseEnd && response.schema instanceof zod_1.z.ZodVoid) {
|
|
13
35
|
return;
|
|
14
36
|
}
|
|
15
37
|
const isAlreadyPresent = !!responses[String(response.status)];
|
|
@@ -81,7 +103,129 @@ const forcePathParamsRequired = (spec) => {
|
|
|
81
103
|
});
|
|
82
104
|
};
|
|
83
105
|
exports.forcePathParamsRequired = forcePathParamsRequired;
|
|
84
|
-
const
|
|
106
|
+
const resolveSchemaRef = (node, schemas) => {
|
|
107
|
+
if (node && typeof node === 'object' && typeof node.$ref === 'string') {
|
|
108
|
+
return schemas[node.$ref.split('/').pop()];
|
|
109
|
+
}
|
|
110
|
+
return node;
|
|
111
|
+
};
|
|
112
|
+
// Two same-named properties across anyOf branches are only safe to assert at the
|
|
113
|
+
// parent level if they agree on primitive type — an enum/const literal that differs
|
|
114
|
+
// per branch (e.g. a discriminating `error` code) must NOT leak into the hoisted
|
|
115
|
+
// type, or the parent schema would wrongly forbid the other branches' literals.
|
|
116
|
+
const widenAnyOfProperty = (fragments) => {
|
|
117
|
+
if (fragments.some(f => f === undefined || typeof f !== 'object')) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const types = new Set(fragments.map(f => f.type));
|
|
121
|
+
if (types.size !== 1 || fragments[0].type === undefined) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const widened = { type: fragments[0].type };
|
|
125
|
+
if (fragments.some(f => f.nullable)) {
|
|
126
|
+
widened.nullable = true;
|
|
127
|
+
}
|
|
128
|
+
return widened;
|
|
129
|
+
};
|
|
130
|
+
// anyOf is OR — an object only ever matches ONE branch, so a field is safe to
|
|
131
|
+
// assert at the parent only if every branch requires it, and its type is widened
|
|
132
|
+
// to what every branch agrees on (see widenAnyOfProperty). A field required in
|
|
133
|
+
// some but not all branches must stay inside the combinator: hoisting it would
|
|
134
|
+
// assert something false for the branches that lack it, and would wrongly
|
|
135
|
+
// suppress a real breaking-change finding on that branch in the future.
|
|
136
|
+
const hoistAnyOf = (node, schemas) => {
|
|
137
|
+
const branches = node.anyOf.map(b => resolveSchemaRef(b, schemas));
|
|
138
|
+
if (branches.length === 0 ||
|
|
139
|
+
branches.some(b => !b || typeof b !== 'object')) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const requiredSets = branches.map(b => new Set(b.required || []));
|
|
143
|
+
const commonRequiredNames = [...requiredSets[0]].filter(name => requiredSets.every(set => set.has(name)));
|
|
144
|
+
const hoistedProperties = {};
|
|
145
|
+
const hoistedRequired = [];
|
|
146
|
+
for (const name of commonRequiredNames) {
|
|
147
|
+
const fragments = branches.map(b => b.properties?.[name]);
|
|
148
|
+
const widened = widenAnyOfProperty(fragments);
|
|
149
|
+
if (widened) {
|
|
150
|
+
hoistedProperties[name] = widened;
|
|
151
|
+
hoistedRequired.push(name);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (hoistedRequired.length === 0) {
|
|
155
|
+
return; // nothing intersects safely — leave the combinator untouched, no-op
|
|
156
|
+
}
|
|
157
|
+
node.type = 'object';
|
|
158
|
+
node.properties = { ...(node.properties || {}), ...hoistedProperties };
|
|
159
|
+
node.required = [...new Set([...(node.required || []), ...hoistedRequired])];
|
|
160
|
+
};
|
|
161
|
+
// allOf is AND — an object must match every branch simultaneously, so unioning
|
|
162
|
+
// every branch's properties/required to the parent is always safe: nothing is
|
|
163
|
+
// asserted that isn't already true of any value satisfying the allOf. No type
|
|
164
|
+
// widening needed (unlike anyOf) since branches aren't alternatives.
|
|
165
|
+
const hoistAllOf = (node, schemas) => {
|
|
166
|
+
const branches = node.allOf.map(b => resolveSchemaRef(b, schemas));
|
|
167
|
+
if (branches.some(b => !b || typeof b !== 'object')) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const unionProperties = {};
|
|
171
|
+
const unionRequired = new Set();
|
|
172
|
+
for (const branch of branches) {
|
|
173
|
+
for (const [key, value] of Object.entries(branch.properties || {})) {
|
|
174
|
+
if (!(key in unionProperties)) {
|
|
175
|
+
unionProperties[key] = value;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const name of branch.required || []) {
|
|
179
|
+
unionRequired.add(name);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (Object.keys(unionProperties).length === 0) {
|
|
183
|
+
return; // nothing to hoist — leave the combinator untouched, no-op
|
|
184
|
+
}
|
|
185
|
+
node.type = 'object';
|
|
186
|
+
node.properties = { ...(node.properties || {}), ...unionProperties };
|
|
187
|
+
node.required = [...new Set([...(node.required || []), ...unionRequired])];
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* zod-to-openapi represents a same-status multi-error response as `anyOf`, and
|
|
191
|
+
* `.extend()` on a registered/ref'd schema as `allOf: [$ref, inline]`. Either
|
|
192
|
+
* shape hides required/property information that used to be flat at the parent
|
|
193
|
+
* level inside the combinator, which oasdiff's breaking-change diff cannot see
|
|
194
|
+
* through — it reports every such change as fields being removed, even when the
|
|
195
|
+
* change was purely additive. This walks the finished spec and restores the
|
|
196
|
+
* safely-hoistable subset of that information to the parent schema (see
|
|
197
|
+
* hoistAnyOf / hoistAllOf for what "safe" means for each combinator — they are
|
|
198
|
+
* NOT the same rule). Ref topology is untouched: refs are only read to compute
|
|
199
|
+
* what's safe to hoist, never rewritten.
|
|
200
|
+
*/
|
|
201
|
+
const hoistCombinatorRequiredFields = (spec) => {
|
|
202
|
+
const schemas = (spec.components?.schemas || {});
|
|
203
|
+
const visit = (node) => {
|
|
204
|
+
if (!node || typeof node !== 'object') {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(node)) {
|
|
208
|
+
node.forEach(visit);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const obj = node;
|
|
212
|
+
if (Array.isArray(obj.anyOf)) {
|
|
213
|
+
obj.anyOf.forEach(visit);
|
|
214
|
+
hoistAnyOf(obj, schemas);
|
|
215
|
+
}
|
|
216
|
+
else if (Array.isArray(obj.allOf)) {
|
|
217
|
+
obj.allOf.forEach(visit);
|
|
218
|
+
hoistAllOf(obj, schemas);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
Object.values(obj).forEach(visit);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
visit(spec.paths || {});
|
|
225
|
+
visit(schemas);
|
|
226
|
+
};
|
|
227
|
+
exports.hoistCombinatorRequiredFields = hoistCombinatorRequiredFields;
|
|
228
|
+
const registerEndpoint = (registry, method, prefixPath, rootPrefixPath, path, summary,
|
|
85
229
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
230
|
options, tags, debug) => {
|
|
87
231
|
let bodySchema = options.schemas?.body || undefined;
|
|
@@ -89,6 +233,7 @@ options, tags, debug) => {
|
|
|
89
233
|
let querySchema = options.schemas?.query || undefined;
|
|
90
234
|
let headersSchema = options.schemas?.headers || undefined;
|
|
91
235
|
let responseSchemas = options.responses;
|
|
236
|
+
const securitySchemes = new Set();
|
|
92
237
|
if (options.errors) {
|
|
93
238
|
for (const errorType of Object.keys(options.errors)) {
|
|
94
239
|
const statusCode = options.errors[errorType];
|
|
@@ -149,6 +294,9 @@ options, tags, debug) => {
|
|
|
149
294
|
});
|
|
150
295
|
}
|
|
151
296
|
}
|
|
297
|
+
for (const scheme of middleware.options.security || []) {
|
|
298
|
+
securitySchemes.add(scheme);
|
|
299
|
+
}
|
|
152
300
|
}
|
|
153
301
|
// Add ZodError response
|
|
154
302
|
responseSchemas.push({
|
|
@@ -205,7 +353,11 @@ options, tags, debug) => {
|
|
|
205
353
|
headers: headersSchema ? [headersSchema] : [],
|
|
206
354
|
},
|
|
207
355
|
responses: responseMap,
|
|
208
|
-
|
|
356
|
+
operationId: options.operationId ||
|
|
357
|
+
deriveOperationId(registry, method, prefixPath.slice(rootPrefixPath.length) + path),
|
|
358
|
+
...(securitySchemes.size > 0 && {
|
|
359
|
+
security: [...securitySchemes].map(scheme => ({ [scheme]: [] })),
|
|
360
|
+
}),
|
|
209
361
|
});
|
|
210
362
|
};
|
|
211
363
|
exports.registerEndpoint = registerEndpoint;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SECURITY_SCHEMES = void 0;
|
|
4
|
+
const X_AUTH_HEADER = {
|
|
5
|
+
type: 'apiKey',
|
|
6
|
+
in: 'header',
|
|
7
|
+
name: 'X-Auth',
|
|
8
|
+
};
|
|
9
|
+
exports.SECURITY_SCHEMES = {
|
|
10
|
+
jwt_operator: X_AUTH_HEADER,
|
|
11
|
+
jwt_support: X_AUTH_HEADER,
|
|
12
|
+
jwt_operator_device: X_AUTH_HEADER,
|
|
13
|
+
jwt_consumer: X_AUTH_HEADER,
|
|
14
|
+
api_key: { type: 'apiKey', in: 'header', name: 'X-Api-Key' },
|
|
15
|
+
};
|
|
@@ -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>;
|
|
@@ -43,6 +43,18 @@ export type Payment = PaymentBase & {
|
|
|
43
43
|
};
|
|
44
44
|
export declare const PAYMENT_EVENT_VERSION = 2;
|
|
45
45
|
export type PaymentEventVersion = typeof PAYMENT_EVENT_VERSION;
|
|
46
|
+
/**
|
|
47
|
+
* Stable acquirer snapshot fields for CARD fiscalization (LUCA-35099).
|
|
48
|
+
* Populated by backend-pay when available on V3Payment at produce time.
|
|
49
|
+
* Fees are intentionally omitted — pay remains source of truth at report/close.
|
|
50
|
+
*/
|
|
51
|
+
export type PaymentEventCardAcquirerFields = {
|
|
52
|
+
cardLast4?: string | null;
|
|
53
|
+
authCode?: string | null;
|
|
54
|
+
cardScheme?: string | null;
|
|
55
|
+
cardBrandLabel?: string | null;
|
|
56
|
+
terminalId?: string | null;
|
|
57
|
+
};
|
|
46
58
|
export type PaymentEvent = {
|
|
47
59
|
version: PaymentEventVersion;
|
|
48
60
|
uuid: string;
|
|
@@ -60,7 +72,7 @@ export type PaymentEvent = {
|
|
|
60
72
|
orderId?: string;
|
|
61
73
|
statusCompleteAt?: Date | null;
|
|
62
74
|
merchantDetails?: PaymentMerchantDetails;
|
|
63
|
-
};
|
|
75
|
+
} & PaymentEventCardAcquirerFields;
|
|
64
76
|
export declare const consumerPayAmount: (event: PaymentEvent) => number;
|
|
65
77
|
export declare const operatorReceivableAmount: (event: PaymentEvent) => number;
|
|
66
78
|
export type PaymentMerchantDetails = {
|
|
@@ -18,9 +18,44 @@ type LanguageOptions = typeof AVAILABLE_LANGUAGES[number];
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
export declare const isInternalLanguage: (lang: string) => lang is InternalLanguages;
|
|
21
|
+
/**
|
|
22
|
+
* Canonicalises a language code for storage: `DE-de` -> `de-DE`.
|
|
23
|
+
* Returns null when the value is absent or not a plausible language code.
|
|
24
|
+
*
|
|
25
|
+
* The region is dropped when keeping it would not fit the `STRING(5)` language
|
|
26
|
+
* columns, so `fil-PH` stores as `fil` rather than overflowing or being thrown
|
|
27
|
+
* away — the language is the part we care about.
|
|
28
|
+
*
|
|
29
|
+
* Unlike {@link toInternalLanguage} this does NOT narrow to the languages our
|
|
30
|
+
* templates support — storage keeps what the user actually asked for, and
|
|
31
|
+
* narrowing happens on retrieval.
|
|
32
|
+
*/
|
|
33
|
+
export declare const normalizeStoredLanguage: (lang?: string | null) => string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Reads the caller's preferred language without narrowing it to the languages
|
|
36
|
+
* our templates support. Use for values that get persisted; use
|
|
37
|
+
* {@link getPreferredLanguageFromRequestHeaders} when the result immediately
|
|
38
|
+
* selects a template.
|
|
39
|
+
*
|
|
40
|
+
* Returns null when no usable `Accept-Language` was sent, so callers can leave
|
|
41
|
+
* the column untouched rather than writing a guessed default.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* getStoredLanguageFromRequestHeaders({ 'accept-language': 'fr-FR,fr;q=0.9' }) // 'fr-FR'
|
|
46
|
+
* getStoredLanguageFromRequestHeaders({}) // null
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare const getStoredLanguageFromRequestHeaders: (headers?: Record<string, string>) => string | null;
|
|
50
|
+
export declare const getStoredLanguageFromRequest: (request: Request) => string | null;
|
|
21
51
|
/**
|
|
22
52
|
* Safely converts a language code to an InternalLanguage.
|
|
23
|
-
*
|
|
53
|
+
* The region subtag is dropped before matching, so `de-DE` resolves to `de`
|
|
54
|
+
* rather than falling back. If the language is not supported, falls back to 'en'.
|
|
55
|
+
*
|
|
56
|
+
* This is the retrieval-side counterpart to {@link normalizeStoredLanguage}:
|
|
57
|
+
* columns may hold any language the user asked for, and every read that feeds
|
|
58
|
+
* a template goes through here.
|
|
24
59
|
*
|
|
25
60
|
* @param lang - Language code to convert
|
|
26
61
|
* @returns The language if supported, otherwise 'en'
|
|
@@ -28,11 +63,12 @@ export declare const isInternalLanguage: (lang: string) => lang is InternalLangu
|
|
|
28
63
|
* @example
|
|
29
64
|
* ```typescript
|
|
30
65
|
* toInternalLanguage('de') // returns 'de'
|
|
66
|
+
* toInternalLanguage('de-DE') // returns 'de'
|
|
31
67
|
* toInternalLanguage('en') // returns 'en'
|
|
32
68
|
* toInternalLanguage('fr') // returns 'en' (fallback)
|
|
33
69
|
* ```
|
|
34
70
|
*/
|
|
35
|
-
export declare const toInternalLanguage: (lang
|
|
71
|
+
export declare const toInternalLanguage: (lang?: string | null) => InternalLanguages;
|
|
36
72
|
export declare const parseAcceptLanguageHeader: (header?: string) => LanguageOptions;
|
|
37
73
|
/**
|
|
38
74
|
* @deprecated Use getPreferredLanguageFromRequestHeaders instead.
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getPreferredLanguageFromRequestHeaders = exports.getPreferredLanguageFromRequest = exports.parseAcceptLanguageHeader = exports.toInternalLanguage = exports.isInternalLanguage = exports.AVAILABLE_LANGUAGES = exports.DEFAULT_LANGUAGE_WHEN_HEADER = exports.DEFAULT_LANGUAGE_WHEN_NO_HEADER = void 0;
|
|
6
|
+
exports.getPreferredLanguageFromRequestHeaders = exports.getPreferredLanguageFromRequest = exports.parseAcceptLanguageHeader = exports.toInternalLanguage = exports.getStoredLanguageFromRequest = exports.getStoredLanguageFromRequestHeaders = exports.normalizeStoredLanguage = exports.isInternalLanguage = exports.AVAILABLE_LANGUAGES = exports.DEFAULT_LANGUAGE_WHEN_HEADER = exports.DEFAULT_LANGUAGE_WHEN_NO_HEADER = void 0;
|
|
7
7
|
const negotiator_1 = __importDefault(require("negotiator"));
|
|
8
8
|
exports.DEFAULT_LANGUAGE_WHEN_NO_HEADER = 'de';
|
|
9
9
|
exports.DEFAULT_LANGUAGE_WHEN_HEADER = 'en';
|
|
@@ -25,9 +25,80 @@ const isInternalLanguage = (lang) => {
|
|
|
25
25
|
return exports.AVAILABLE_LANGUAGES.includes(lang);
|
|
26
26
|
};
|
|
27
27
|
exports.isInternalLanguage = isInternalLanguage;
|
|
28
|
+
/**
|
|
29
|
+
* Shape of a language code we are willing to persist: a 2-3 letter primary
|
|
30
|
+
* subtag, optionally followed by a 2-letter region. Deliberately narrower than
|
|
31
|
+
* full BCP 47 so an attacker-controlled `Accept-Language` cannot write junk;
|
|
32
|
+
* script subtags (`zh-Hant`) are rejected rather than truncated.
|
|
33
|
+
*/
|
|
34
|
+
const STORED_LANGUAGE_PATTERN = /^[a-z]{2,3}(-[a-z]{2})?$/i;
|
|
35
|
+
/**
|
|
36
|
+
* Longest value the `language` columns accept — they are `STRING(5)`, which
|
|
37
|
+
* fits `pt-BR` but not a 3-letter primary subtag with a region (`fil-PH`).
|
|
38
|
+
*/
|
|
39
|
+
const STORED_LANGUAGE_MAX_LENGTH = 5;
|
|
40
|
+
/**
|
|
41
|
+
* Canonicalises a language code for storage: `DE-de` -> `de-DE`.
|
|
42
|
+
* Returns null when the value is absent or not a plausible language code.
|
|
43
|
+
*
|
|
44
|
+
* The region is dropped when keeping it would not fit the `STRING(5)` language
|
|
45
|
+
* columns, so `fil-PH` stores as `fil` rather than overflowing or being thrown
|
|
46
|
+
* away — the language is the part we care about.
|
|
47
|
+
*
|
|
48
|
+
* Unlike {@link toInternalLanguage} this does NOT narrow to the languages our
|
|
49
|
+
* templates support — storage keeps what the user actually asked for, and
|
|
50
|
+
* narrowing happens on retrieval.
|
|
51
|
+
*/
|
|
52
|
+
const normalizeStoredLanguage = (lang) => {
|
|
53
|
+
const trimmed = lang?.trim();
|
|
54
|
+
if (!trimmed || !STORED_LANGUAGE_PATTERN.test(trimmed)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const [primary, region] = trimmed.split('-');
|
|
58
|
+
const canonical = region
|
|
59
|
+
? `${primary.toLowerCase()}-${region.toUpperCase()}`
|
|
60
|
+
: primary.toLowerCase();
|
|
61
|
+
return canonical.length > STORED_LANGUAGE_MAX_LENGTH
|
|
62
|
+
? primary.toLowerCase()
|
|
63
|
+
: canonical;
|
|
64
|
+
};
|
|
65
|
+
exports.normalizeStoredLanguage = normalizeStoredLanguage;
|
|
66
|
+
/**
|
|
67
|
+
* Reads the caller's preferred language without narrowing it to the languages
|
|
68
|
+
* our templates support. Use for values that get persisted; use
|
|
69
|
+
* {@link getPreferredLanguageFromRequestHeaders} when the result immediately
|
|
70
|
+
* selects a template.
|
|
71
|
+
*
|
|
72
|
+
* Returns null when no usable `Accept-Language` was sent, so callers can leave
|
|
73
|
+
* the column untouched rather than writing a guessed default.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* getStoredLanguageFromRequestHeaders({ 'accept-language': 'fr-FR,fr;q=0.9' }) // 'fr-FR'
|
|
78
|
+
* getStoredLanguageFromRequestHeaders({}) // null
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
const getStoredLanguageFromRequestHeaders = (headers) => {
|
|
82
|
+
const acceptLanguageHeader = headers?.['accept-language'];
|
|
83
|
+
if (!acceptLanguageHeader) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
// Take the highest-priority entry we can actually store, rather than giving
|
|
87
|
+
// up when the first one happens to be unstorable (e.g. `zh-Hant-TW, zh-TW`).
|
|
88
|
+
const negotiator = new negotiator_1.default({ headers });
|
|
89
|
+
return (negotiator.languages().map(exports.normalizeStoredLanguage).find(Boolean) ?? null);
|
|
90
|
+
};
|
|
91
|
+
exports.getStoredLanguageFromRequestHeaders = getStoredLanguageFromRequestHeaders;
|
|
92
|
+
const getStoredLanguageFromRequest = (request) => (0, exports.getStoredLanguageFromRequestHeaders)(request.headers);
|
|
93
|
+
exports.getStoredLanguageFromRequest = getStoredLanguageFromRequest;
|
|
28
94
|
/**
|
|
29
95
|
* Safely converts a language code to an InternalLanguage.
|
|
30
|
-
*
|
|
96
|
+
* The region subtag is dropped before matching, so `de-DE` resolves to `de`
|
|
97
|
+
* rather than falling back. If the language is not supported, falls back to 'en'.
|
|
98
|
+
*
|
|
99
|
+
* This is the retrieval-side counterpart to {@link normalizeStoredLanguage}:
|
|
100
|
+
* columns may hold any language the user asked for, and every read that feeds
|
|
101
|
+
* a template goes through here.
|
|
31
102
|
*
|
|
32
103
|
* @param lang - Language code to convert
|
|
33
104
|
* @returns The language if supported, otherwise 'en'
|
|
@@ -35,12 +106,14 @@ exports.isInternalLanguage = isInternalLanguage;
|
|
|
35
106
|
* @example
|
|
36
107
|
* ```typescript
|
|
37
108
|
* toInternalLanguage('de') // returns 'de'
|
|
109
|
+
* toInternalLanguage('de-DE') // returns 'de'
|
|
38
110
|
* toInternalLanguage('en') // returns 'en'
|
|
39
111
|
* toInternalLanguage('fr') // returns 'en' (fallback)
|
|
40
112
|
* ```
|
|
41
113
|
*/
|
|
42
114
|
const toInternalLanguage = (lang) => {
|
|
43
|
-
|
|
115
|
+
const primary = lang?.trim().split('-')[0]?.toLowerCase();
|
|
116
|
+
return primary && (0, exports.isInternalLanguage)(primary) ? primary : 'en';
|
|
44
117
|
};
|
|
45
118
|
exports.toInternalLanguage = toInternalLanguage;
|
|
46
119
|
const parseAcceptLanguageHeader = (header) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lucaapp/service-utils",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.27.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -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.
|
|
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",
|