@kalutskii/foundation 0.7.22 → 0.7.25
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/index.d.ts +463 -429
- package/dist/index.js +954 -644
- package/package.json +8 -9
package/dist/index.js
CHANGED
|
@@ -1,730 +1,1040 @@
|
|
|
1
|
-
// src/drizzle/drizzle.refiners.ts
|
|
2
1
|
import { and, eq } from "drizzle-orm";
|
|
3
|
-
var AT_LEAST_ONE_DEFINED_CONDITION_ERROR = "sqlWhere requires at least one defined condition.";
|
|
4
|
-
function sqlWhere(table, where) {
|
|
5
|
-
const columns = table;
|
|
6
|
-
const conditions = Object.entries(where).filter(([, value]) => value !== void 0).map(([key, value]) => eq(columns[key], value));
|
|
7
|
-
if (conditions.length === 0) {
|
|
8
|
-
throw new Error(AT_LEAST_ONE_DEFINED_CONDITION_ERROR);
|
|
9
|
-
}
|
|
10
|
-
return and(...conditions);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// src/hono/hono.execution.ts
|
|
14
2
|
import { HTTPException } from "hono/http-exception";
|
|
15
|
-
import { red
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
import { blue, bold, dim, green, red, white, yellow } from "kleur/colors";
|
|
4
|
+
import { format } from "date-fns";
|
|
5
|
+
import { getTimezoneOffset, toZonedTime } from "date-fns-tz";
|
|
6
|
+
import { ru } from "date-fns/locale";
|
|
7
|
+
import { z as z$1 } from "zod";
|
|
8
|
+
import { decode, sign, verify } from "hono/jwt";
|
|
9
|
+
//#region src/drizzle/drizzle.refiners.ts
|
|
10
|
+
const AT_LEAST_ONE_DEFINED_CONDITION_ERROR = "sqlWhere requires at least one defined condition.";
|
|
11
|
+
/**
|
|
12
|
+
* Builds a Drizzle `WHERE` clause by combining defined object entries with `and`.
|
|
13
|
+
* Expects the supplied keys to be validated against the table beforehand.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* await db.update(usersTable).set(values).where(sqlWhere(usersTable, { id: 1 })).returning();
|
|
17
|
+
*/
|
|
18
|
+
function sqlWhere(table, where) {
|
|
19
|
+
const columns = table;
|
|
20
|
+
const conditions = Object.entries(where).filter(([, value]) => value !== void 0).map(([key, value]) => eq(columns[key], value));
|
|
21
|
+
if (conditions.length === 0) throw new Error(AT_LEAST_ONE_DEFINED_CONDITION_ERROR);
|
|
22
|
+
return and(...conditions);
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/http/http.factory.ts
|
|
26
|
+
/**
|
|
27
|
+
* Creates a successful API envelope without cloning its data.
|
|
28
|
+
* Generic inference preserves the exact supplied payload type.
|
|
29
|
+
*/
|
|
18
30
|
function success({ status, data }) {
|
|
19
|
-
|
|
31
|
+
return {
|
|
32
|
+
kind: "data",
|
|
33
|
+
status,
|
|
34
|
+
data
|
|
35
|
+
};
|
|
20
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a failed API envelope with one supported exception status.
|
|
39
|
+
* The supplied message remains unchanged for downstream presentation.
|
|
40
|
+
*/
|
|
21
41
|
function failure({ status, error }) {
|
|
22
|
-
|
|
42
|
+
return {
|
|
43
|
+
kind: "error",
|
|
44
|
+
status,
|
|
45
|
+
error
|
|
46
|
+
};
|
|
23
47
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/utilities/datetime.utilities.ts
|
|
50
|
+
/**
|
|
51
|
+
* Projects the current instant onto the wall-clock fields of another timezone.
|
|
52
|
+
* The timezone defaults to `Europe/London` when no option is provided.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const londonTime = getZonedTime({ tz: 'Europe/London' });
|
|
56
|
+
*/
|
|
32
57
|
function getZonedTime({ tz = "Europe/London" } = {}) {
|
|
33
|
-
|
|
58
|
+
return toZonedTime(/* @__PURE__ */ new Date(), tz);
|
|
34
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Formats the UTC offset of a timezone at the supplied date.
|
|
62
|
+
* An explicit date preserves daylight-saving and historical offset rules.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* getUTCOffset(date, 'Europe/Moscow'); // `(+3 UTC)`
|
|
66
|
+
*/
|
|
35
67
|
function getUTCOffset(date, tz) {
|
|
36
|
-
|
|
37
|
-
|
|
68
|
+
const offset = getTimezoneOffset(tz, date) / 36e5;
|
|
69
|
+
return `(${offset >= 0 ? "+" : ""}${offset} UTC)`;
|
|
38
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Formats the current time and UTC offset in the selected timezone.
|
|
73
|
+
* The timezone defaults to `Europe/London` when no option is provided.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* getFormattedTime({ tz: 'Europe/Moscow' }); // `03:04:05 (+3 UTC)`
|
|
77
|
+
*/
|
|
39
78
|
function getFormattedTime({ tz = "Europe/London" } = {}) {
|
|
40
|
-
|
|
41
|
-
|
|
79
|
+
const zonedTime = getZonedTime({ tz });
|
|
80
|
+
return `${format(zonedTime, "HH:mm:ss")} ${getUTCOffset(zonedTime, tz)}`;
|
|
42
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Formats the current date with optional time and UTC offset components.
|
|
84
|
+
* Time is included by default and uses the selected timezone for display.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* getFormattedDate({ tz: 'UTC', withTime: false }); // `02.01.2024`
|
|
88
|
+
*/
|
|
43
89
|
function getFormattedDate({ tz = "Europe/London", withTime = true } = {}) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const formatted = format(zonedTime, pattern);
|
|
47
|
-
return `${formatted}${withTime ? ` ${getUTCOffset(zonedTime, tz)}` : ""}`;
|
|
90
|
+
const zonedTime = getZonedTime({ tz });
|
|
91
|
+
return `${format(zonedTime, withTime ? "dd.MM.yyyy HH:mm:ss" : "dd.MM.yyyy")}${withTime ? ` ${getUTCOffset(zonedTime, tz)}` : ""}`;
|
|
48
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Formats an explicit instant in another timezone using the selected locale.
|
|
95
|
+
* The result includes localized date text, wall-clock time, and UTC offset.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* formatTime(date, { locale: ru, tz: 'Europe/Moscow' });
|
|
99
|
+
*/
|
|
49
100
|
function formatTime(time, { locale = ru, tz = "Europe/London" } = {}) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
101
|
+
const zonedTime = toZonedTime(time, tz);
|
|
102
|
+
return `${format(zonedTime, "HH:mm:ss, d MMMM yyyy", { locale })} ${getUTCOffset(zonedTime, tz)}`;
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/utilities/enums.utilities.ts
|
|
106
|
+
/**
|
|
107
|
+
* Creates an immutable enum-like record from a readonly string array.
|
|
108
|
+
* Keys are uppercased and every dot or hyphen is replaced with an underscore.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* createStringEnumRecord(['foo-bar.s', 'baz'] as const); // `{ FOO_BAR_S: 'foo-bar.s', BAZ: 'baz' }`
|
|
112
|
+
*/
|
|
58
113
|
function createStringEnumRecord(values) {
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
114
|
+
return Object.freeze(Object.fromEntries(values.map((value) => [value.replaceAll(/[.-]/g, "_").toUpperCase(), value])));
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/logging/logging.enums.ts
|
|
118
|
+
const logLevelsArray = [
|
|
119
|
+
"info",
|
|
120
|
+
"warn",
|
|
121
|
+
"error"
|
|
122
|
+
];
|
|
123
|
+
const logLevelsRecord = createStringEnumRecord(logLevelsArray);
|
|
124
|
+
const logLevel = logLevelsRecord;
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/logging/logging.constants.ts
|
|
127
|
+
/**
|
|
128
|
+
* Terminal color configuration for every supported logging level.
|
|
129
|
+
* The `LogLevel` contract prevents missing or unsupported level entries.
|
|
130
|
+
*/
|
|
131
|
+
const logLevelColors = {
|
|
132
|
+
[logLevel.INFO]: blue,
|
|
133
|
+
[logLevel.WARN]: yellow,
|
|
134
|
+
[logLevel.ERROR]: red
|
|
72
135
|
};
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Public placeholder written instead of sensitive query and JSON values.
|
|
138
|
+
* One stable marker keeps redacted output recognizable across log consumers.
|
|
139
|
+
*/
|
|
140
|
+
const REDACTED_LOG_VALUE = "[redacted]";
|
|
141
|
+
/**
|
|
142
|
+
* Normalized key fragments treated as sensitive by logging redaction.
|
|
143
|
+
* Matching ignores case and separators so compound field names remain covered.
|
|
144
|
+
*/
|
|
145
|
+
const sensitiveLogKeyParts = [
|
|
146
|
+
"password",
|
|
147
|
+
"passwd",
|
|
148
|
+
"token",
|
|
149
|
+
"secret",
|
|
150
|
+
"authorization",
|
|
151
|
+
"apikey",
|
|
152
|
+
"credential"
|
|
82
153
|
];
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Terminal colors assigned to the supported HTTP response status ranges.
|
|
156
|
+
* Unlisted ranges intentionally retain the terminal's default text appearance.
|
|
157
|
+
*/
|
|
158
|
+
const httpStatusColors = [
|
|
159
|
+
{
|
|
160
|
+
range: [200, 299],
|
|
161
|
+
color: green
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
range: [400, 499],
|
|
165
|
+
color: yellow
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
range: [500, 599],
|
|
169
|
+
color: red
|
|
170
|
+
}
|
|
87
171
|
];
|
|
88
|
-
|
|
89
|
-
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/logging/logging.services.ts
|
|
90
174
|
function writeLog(message, level, service = "log", stack) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
175
|
+
const timestamp = dim(getFormattedTime());
|
|
176
|
+
const serviceName = logLevelColors[level](service.padEnd(12));
|
|
177
|
+
const formattedMessage = white(message);
|
|
178
|
+
console.log(`[${timestamp}] ${serviceName} | ${formattedMessage}`);
|
|
179
|
+
if (stack) console.log(`[${timestamp}] ${red("↳ trace").padEnd(18)} | ${dim(stack)}`);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Writes timestamped and colorized messages using a stable service column.
|
|
183
|
+
* Error messages may include a stack trace on a subordinate second line.
|
|
184
|
+
*/
|
|
185
|
+
const log = {
|
|
186
|
+
/**
|
|
187
|
+
* Writes an informational message with an optional service label.
|
|
188
|
+
* Missing service names use the shared `log` fallback label.
|
|
189
|
+
*/
|
|
190
|
+
info(message, service) {
|
|
191
|
+
writeLog(message, "info", service);
|
|
192
|
+
},
|
|
193
|
+
/**
|
|
194
|
+
* Writes a warning message with an optional service label.
|
|
195
|
+
* Missing service names use the shared `log` fallback label.
|
|
196
|
+
*/
|
|
197
|
+
warn(message, service) {
|
|
198
|
+
writeLog(message, "warn", service);
|
|
199
|
+
},
|
|
200
|
+
/**
|
|
201
|
+
* Writes an error message with optional service and stack trace context.
|
|
202
|
+
* Provided stack traces are rendered beneath the primary message.
|
|
203
|
+
*/
|
|
204
|
+
error(message, service, stack) {
|
|
205
|
+
writeLog(message, "error", service, stack);
|
|
206
|
+
}
|
|
120
207
|
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/utilities/generation.utilities.ts
|
|
210
|
+
const DEFAULT_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
211
|
+
/**
|
|
212
|
+
* Creates a cryptographically sourced string from the alphanumeric set.
|
|
213
|
+
* Requested length defaults to `10` characters when omitted.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* generateRandomString(5); // `aZ3fG`
|
|
217
|
+
* generateRandomString(); // `G5kLm2P9sQ`
|
|
218
|
+
*/
|
|
124
219
|
function generateRandomString(length = 10) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// src/hono/hono.execution.ts
|
|
220
|
+
const randomValues = crypto.getRandomValues(new Uint32Array(length));
|
|
221
|
+
let result = "";
|
|
222
|
+
for (let i = 0; i < length; i++) result += DEFAULT_CHARACTERS[randomValues[i] % 62];
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/hono/hono.execution.ts
|
|
134
227
|
function proceedUnhandledError(error) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
228
|
+
const errorId = generateRandomString(6);
|
|
229
|
+
const errorMessage = error instanceof Error ? error.stack ?? error.message : JSON.stringify(error);
|
|
230
|
+
log.error(`Unhandled error: ${red(errorMessage)}`, errorId);
|
|
231
|
+
return failure({
|
|
232
|
+
status: 500,
|
|
233
|
+
error: `Internal server error | ${errorId}`
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Converts expected `HTTPException` values into shared API error envelopes.
|
|
238
|
+
* Unexpected failures are logged and represented by a traceable generic response.
|
|
239
|
+
*/
|
|
240
|
+
const onHandlerError = (error, c) => {
|
|
241
|
+
let response;
|
|
242
|
+
if (error instanceof HTTPException && error.status < 500) response = failure({
|
|
243
|
+
status: error.status,
|
|
244
|
+
error: error.message
|
|
245
|
+
});
|
|
246
|
+
else response = proceedUnhandledError(error);
|
|
247
|
+
return c.json(response, response.status);
|
|
147
248
|
};
|
|
148
|
-
|
|
149
|
-
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/hono/hono.respond.ts
|
|
251
|
+
/**
|
|
252
|
+
* Wraps `c.json` in the shared success envelope while preserving its literal status.
|
|
253
|
+
* Missing response data is represented by an empty object for contract consistency.
|
|
254
|
+
*/
|
|
150
255
|
function respond(c, options) {
|
|
151
|
-
|
|
256
|
+
return c.json(success({
|
|
257
|
+
status: options.status,
|
|
258
|
+
data: options.data ?? {}
|
|
259
|
+
}), options.status);
|
|
152
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Responds with downloadable binary content and its attachment headers.
|
|
263
|
+
* Unknown/undefined content types default to `application/octet-stream`.
|
|
264
|
+
*/
|
|
153
265
|
function fileRespond(c, options) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
266
|
+
c.header("Content-Disposition", `attachment; filename="${options.filename}"`);
|
|
267
|
+
c.header("Content-Type", options.contentType ?? "application/octet-stream");
|
|
268
|
+
return c.body(options.content, options.status);
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/hmac/hmac.enums.ts
|
|
272
|
+
const hmacAlgorithmsArray = [
|
|
273
|
+
"SHA-256",
|
|
274
|
+
"SHA-384",
|
|
275
|
+
"SHA-512"
|
|
276
|
+
];
|
|
277
|
+
const hmacAlgorithmsRecord = createStringEnumRecord(hmacAlgorithmsArray);
|
|
278
|
+
const hmacAlgorithm = hmacAlgorithmsRecord;
|
|
279
|
+
const hmacEncodingsArray = [
|
|
280
|
+
"hex",
|
|
281
|
+
"base64",
|
|
282
|
+
"base64url"
|
|
283
|
+
];
|
|
284
|
+
const hmacEncodingsRecord = createStringEnumRecord(hmacEncodingsArray);
|
|
285
|
+
const hmacEncoding = hmacEncodingsRecord;
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/hmac/hmac.constants.ts
|
|
288
|
+
/**
|
|
289
|
+
* Default digest algorithm used by `HMACService` when none is configured.
|
|
290
|
+
* SHA-256 provides broad Web Crypto support and a 256-bit authentication tag.
|
|
291
|
+
*/
|
|
292
|
+
const DEFAULT_HMAC_ALGORITHM = hmacAlgorithm.SHA_256;
|
|
293
|
+
/**
|
|
294
|
+
* Default textual encoding used for signatures returned by `HMACService`.
|
|
295
|
+
* Hex output remains deterministic, portable, and independent of padding rules.
|
|
296
|
+
*/
|
|
297
|
+
const DEFAULT_HMAC_ENCODING = hmacEncoding.HEX;
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/utilities/encoding.utilities.ts
|
|
300
|
+
const BASE64_BYTE_CHUNK_SIZE = 32768;
|
|
301
|
+
/**
|
|
302
|
+
* Encodes arbitrary binary bytes into their canonical Base64 representation.
|
|
303
|
+
* Chunked conversion avoids the argument limit imposed by `String.fromCharCode`.
|
|
304
|
+
*/
|
|
173
305
|
function encodeBase64(bytes) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
return btoa(binary);
|
|
306
|
+
let binary = "";
|
|
307
|
+
for (let offset = 0; offset < bytes.length; offset += BASE64_BYTE_CHUNK_SIZE) binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_BYTE_CHUNK_SIZE));
|
|
308
|
+
return btoa(binary);
|
|
179
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Decodes a canonical or padded Base64 string into its original binary bytes.
|
|
312
|
+
* Invalid input preserves the native `atob` failure instead of returning partial data.
|
|
313
|
+
*/
|
|
180
314
|
function decodeBase64(value) {
|
|
181
|
-
|
|
182
|
-
|
|
315
|
+
const binary = atob(value);
|
|
316
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
183
317
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/hmac/hmac.utilities.ts
|
|
320
|
+
const textEncoder = new TextEncoder();
|
|
321
|
+
/**
|
|
322
|
+
* Converts an HMAC payload or secret into an isolated byte array representation.
|
|
323
|
+
* Strings use UTF-8 while supplied bytes are copied to prevent later mutation.
|
|
324
|
+
*/
|
|
187
325
|
function toHMACBytes(input) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return bytes;
|
|
326
|
+
if (typeof input === "string") return textEncoder.encode(input);
|
|
327
|
+
const bytes = new Uint8Array(input.length);
|
|
328
|
+
bytes.set(input);
|
|
329
|
+
return bytes;
|
|
193
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Encodes binary HMAC output with one supported transport-safe representation.
|
|
333
|
+
* Hex output remains lowercase while Base64 URL output omits conventional padding.
|
|
334
|
+
*/
|
|
194
335
|
function encodeHMACSignature(signature, encoding) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (encoding === "base64")
|
|
200
|
-
return base64;
|
|
201
|
-
return base64.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
336
|
+
if (encoding === "hex") return Array.from(signature, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
337
|
+
const base64 = encodeBase64(signature);
|
|
338
|
+
if (encoding === "base64") return base64;
|
|
339
|
+
return base64.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
202
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Decodes a textual HMAC signature into bytes for cryptographic verification.
|
|
343
|
+
* Malformed alphabets, lengths, or padding combinations reject with `TypeError`.
|
|
344
|
+
*/
|
|
203
345
|
function decodeHMACSignature(signature, encoding) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// src/hmac/hmac.services.ts
|
|
346
|
+
if (encoding === "hex") {
|
|
347
|
+
if (signature.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(signature)) throw new TypeError("Invalid hexadecimal HMAC signature");
|
|
348
|
+
return Uint8Array.from(signature.match(/.{2}/g) ?? [], (byte) => Number.parseInt(byte, 16));
|
|
349
|
+
}
|
|
350
|
+
if (encoding === "base64") {
|
|
351
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(signature)) throw new TypeError("Invalid Base64 HMAC signature");
|
|
352
|
+
return decodeBase64(signature);
|
|
353
|
+
}
|
|
354
|
+
if (!/^[A-Za-z0-9_-]*$/.test(signature) || signature.length % 4 === 1) throw new TypeError("Invalid Base64 URL HMAC signature");
|
|
355
|
+
return decodeBase64(signature.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(signature.length / 4) * 4, "="));
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/hmac/hmac.services.ts
|
|
359
|
+
/**
|
|
360
|
+
* Creates and verifies keyed message authentication codes through Web Crypto.
|
|
361
|
+
* Each instance preserves its digest algorithm and textual encoding configuration.
|
|
362
|
+
*/
|
|
224
363
|
var HMACService = class {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
364
|
+
algorithm;
|
|
365
|
+
encoding;
|
|
366
|
+
constructor(options = {}) {
|
|
367
|
+
this.algorithm = options.algorithm ?? DEFAULT_HMAC_ALGORITHM;
|
|
368
|
+
this.encoding = options.encoding ?? DEFAULT_HMAC_ENCODING;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Authenticates a payload with a secret and returns the encoded signature.
|
|
372
|
+
* String inputs use UTF-8 while byte arrays preserve their exact byte sequence.
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* const signature = await hmacService.sign('payload', 'shared-secret');
|
|
376
|
+
*/
|
|
377
|
+
async sign(payload, secret) {
|
|
378
|
+
const key = await this.importSecret(secret);
|
|
379
|
+
const signature = await crypto.subtle.sign("HMAC", key, toHMACBytes(payload));
|
|
380
|
+
return encodeHMACSignature(new Uint8Array(signature), this.encoding);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Verifies an encoded signature without requiring a manual equality comparison.
|
|
384
|
+
* Invalid encodings, incorrect secrets, and altered payloads all resolve to `false`.
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* const verified = await hmacService.verify('payload', signature, 'shared-secret');
|
|
388
|
+
*/
|
|
389
|
+
async verify(payload, signature, secret) {
|
|
390
|
+
let signatureBytes;
|
|
391
|
+
try {
|
|
392
|
+
signatureBytes = decodeHMACSignature(signature, this.encoding);
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
const key = await this.importSecret(secret);
|
|
397
|
+
return crypto.subtle.verify("HMAC", key, signatureBytes, toHMACBytes(payload));
|
|
398
|
+
}
|
|
399
|
+
async importSecret(secret) {
|
|
400
|
+
const secretBytes = toHMACBytes(secret);
|
|
401
|
+
const algorithm = {
|
|
402
|
+
name: "HMAC",
|
|
403
|
+
hash: this.algorithm
|
|
404
|
+
};
|
|
405
|
+
return crypto.subtle.importKey("raw", secretBytes, algorithm, false, ["sign", "verify"]);
|
|
406
|
+
}
|
|
266
407
|
};
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/http/http.constants.ts
|
|
410
|
+
const SUCCESS_STATUS_CODES = [
|
|
411
|
+
200,
|
|
412
|
+
201,
|
|
413
|
+
202,
|
|
414
|
+
307
|
|
415
|
+
];
|
|
416
|
+
const EXCEPTION_STATUS_CODES = [
|
|
417
|
+
400,
|
|
418
|
+
401,
|
|
419
|
+
403,
|
|
420
|
+
404,
|
|
421
|
+
405,
|
|
422
|
+
409,
|
|
423
|
+
500
|
|
424
|
+
];
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/http/http.resolvers.ts
|
|
427
|
+
/**
|
|
428
|
+
* Converts an API contract envelope into a mutually exclusive safe result.
|
|
429
|
+
* Only `APIError` values are normalized; rejected fetchers still reject.
|
|
430
|
+
*/
|
|
273
431
|
async function fetchSafely(fetcher) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
432
|
+
const response = await fetcher();
|
|
433
|
+
if (response.kind === "error") return {
|
|
434
|
+
error: response.error,
|
|
435
|
+
data: null
|
|
436
|
+
};
|
|
437
|
+
return {
|
|
438
|
+
error: null,
|
|
439
|
+
data: response.data
|
|
440
|
+
};
|
|
278
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Returns successful API data and throws for an `APIError` envelope.
|
|
444
|
+
* Rejected fetchers propagate their original error without replacement.
|
|
445
|
+
*/
|
|
279
446
|
async function fetchAndThrow(fetcher) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
447
|
+
const response = await fetcher();
|
|
448
|
+
if (response.kind === "error") throw new Error(response.error);
|
|
449
|
+
return response.data;
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/logging/logging.security.ts
|
|
453
|
+
/**
|
|
454
|
+
* Checks whether a logging field name contains a configured sensitive fragment.
|
|
455
|
+
* Matching ignores casing and separators to cover compound naming conventions.
|
|
456
|
+
*/
|
|
290
457
|
function isSensitiveLogKey(key) {
|
|
291
|
-
|
|
292
|
-
|
|
458
|
+
const normalizedKey = key.toLowerCase().replaceAll(/[^a-z]/g, "");
|
|
459
|
+
return sensitiveLogKeyParts.some((sensitivePart) => normalizedKey.includes(sensitivePart));
|
|
293
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Recursively replaces values owned by sensitive object keys with the public marker.
|
|
463
|
+
* The result reports whether any replacement occurred to avoid needless serialization.
|
|
464
|
+
*/
|
|
294
465
|
function redactSensitiveValue(value) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
466
|
+
if (Array.isArray(value)) {
|
|
467
|
+
const entries = value.map(redactSensitiveValue);
|
|
468
|
+
return {
|
|
469
|
+
value: entries.map((entry) => entry.value),
|
|
470
|
+
redacted: entries.some((entry) => entry.redacted)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
if (typeof value !== "object" || value === null) return {
|
|
474
|
+
value,
|
|
475
|
+
redacted: false
|
|
476
|
+
};
|
|
477
|
+
let redacted = false;
|
|
478
|
+
const entries = Object.entries(value).map(([key, entryValue]) => {
|
|
479
|
+
if (isSensitiveLogKey(key)) {
|
|
480
|
+
redacted = true;
|
|
481
|
+
return [key, REDACTED_LOG_VALUE];
|
|
482
|
+
}
|
|
483
|
+
const nestedEntry = redactSensitiveValue(entryValue);
|
|
484
|
+
redacted ||= nestedEntry.redacted;
|
|
485
|
+
return [key, nestedEntry.value];
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
value: Object.fromEntries(entries),
|
|
489
|
+
redacted
|
|
490
|
+
};
|
|
315
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* Replaces sensitive query values using partial, case-insensitive key matching.
|
|
494
|
+
* A new collection is returned so the caller's search parameters remain unchanged.
|
|
495
|
+
*/
|
|
316
496
|
function redactSensitiveSearchParams(searchParams) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
redactedSearchParams.set(key, REDACTED_LOG_VALUE);
|
|
321
|
-
}
|
|
322
|
-
return redactedSearchParams;
|
|
497
|
+
const redactedSearchParams = new URLSearchParams(searchParams);
|
|
498
|
+
for (const key of new Set(redactedSearchParams.keys())) if (isSensitiveLogKey(key)) redactedSearchParams.set(key, REDACTED_LOG_VALUE);
|
|
499
|
+
return redactedSearchParams;
|
|
323
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Replaces values under sensitive keys throughout a serialized JSON structure.
|
|
503
|
+
* Invalid JSON and payloads without matching keys are returned byte-for-byte unchanged.
|
|
504
|
+
*/
|
|
324
505
|
function redactSensitiveJSON(json) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
506
|
+
if (json === void 0 || null) return json;
|
|
507
|
+
try {
|
|
508
|
+
const result = redactSensitiveValue(JSON.parse(json));
|
|
509
|
+
return result.redacted ? JSON.stringify(result.value) : json;
|
|
510
|
+
} catch {
|
|
511
|
+
return json;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/logging/logging.utilities.ts
|
|
516
|
+
/**
|
|
517
|
+
* Selects a terminal color function for one HTTP response status.
|
|
518
|
+
* Successes are green, client errors yellow, and server errors red.
|
|
519
|
+
*/
|
|
337
520
|
function getColoredHTTPStatus(status) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
521
|
+
const statusColor = httpStatusColors.find(({ range: [minimum, maximum] }) => {
|
|
522
|
+
return status >= minimum && status <= maximum;
|
|
523
|
+
});
|
|
524
|
+
return statusColor ? statusColor.color : (text) => text;
|
|
525
|
+
}
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/logging/logging.middleware.ts
|
|
528
|
+
/**
|
|
529
|
+
* Logs Hono request metadata with query parameters and a normalized body preview.
|
|
530
|
+
* Sensitive values are redacted by partial key match before output is written.
|
|
531
|
+
*/
|
|
532
|
+
const loggingMiddleware = async (c, next) => {
|
|
533
|
+
const contentType = c.req.header("content-type");
|
|
534
|
+
const searchParams = redactSensitiveSearchParams(new URL(c.req.url).searchParams).toString();
|
|
535
|
+
const body = contentType?.includes("multipart/form-data") ? "[multipart]" : redactSensitiveJSON((await c.req.raw.clone().text()).replaceAll(/\s+/g, " ").trim());
|
|
536
|
+
const bodyPreview = body.length > 60 ? `${body.slice(0, 30)}…${body.slice(-30)}` : body;
|
|
537
|
+
const startTime = performance.now();
|
|
538
|
+
await next();
|
|
539
|
+
const duration = Math.round(performance.now() - startTime);
|
|
540
|
+
const coloredMethod = bold(blue(c.req.method.padEnd(4)));
|
|
541
|
+
const coloredStatus = getColoredHTTPStatus(c.res.status)(String(c.res.status).padEnd(4));
|
|
542
|
+
const coloredTime = dim(`${duration}ms`.padStart(6));
|
|
543
|
+
const coloredPath = white(c.req.path.padEnd(32));
|
|
544
|
+
const coloredSearchParams = searchParams ? dim(` (${searchParams})`) : "";
|
|
545
|
+
const coloredBody = bodyPreview ? dim(` ${bodyPreview}`) : "";
|
|
546
|
+
log.info(`${coloredMethod} ${coloredStatus} ${coloredTime} ${coloredPath}${coloredSearchParams}${coloredBody}`, "hono");
|
|
360
547
|
};
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/upload/upload.enums.ts
|
|
550
|
+
const fileFormatsArray = [...[
|
|
551
|
+
"png",
|
|
552
|
+
"jpg",
|
|
553
|
+
"webp",
|
|
554
|
+
"avif",
|
|
555
|
+
"heic",
|
|
556
|
+
"svg"
|
|
557
|
+
], ...[
|
|
558
|
+
"pdf",
|
|
559
|
+
"rtf",
|
|
560
|
+
"txt"
|
|
561
|
+
]];
|
|
562
|
+
const fileFormatsRecord = createStringEnumRecord(fileFormatsArray);
|
|
563
|
+
const fileFormat = fileFormatsRecord;
|
|
564
|
+
const uploadValidationErrorsArray = [
|
|
565
|
+
"empty_file",
|
|
566
|
+
"unsupported_file_format",
|
|
567
|
+
"file_size_exceeded",
|
|
568
|
+
"files_count_exceeded"
|
|
368
569
|
];
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
570
|
+
const uploadValidationErrorsRecord = createStringEnumRecord(uploadValidationErrorsArray);
|
|
571
|
+
const uploadValidationError = uploadValidationErrorsRecord;
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/upload/upload.constants.ts
|
|
574
|
+
/**
|
|
575
|
+
* Canonical metadata shared by upload controls and runtime validation.
|
|
576
|
+
* Every supported format defines display, MIME, and extension values.
|
|
577
|
+
*/
|
|
578
|
+
const fileFormatsConfig = {
|
|
579
|
+
[fileFormatsRecord.PNG]: {
|
|
580
|
+
name: "PNG",
|
|
581
|
+
mimeTypes: ["image/png"],
|
|
582
|
+
extensions: [".png"]
|
|
583
|
+
},
|
|
584
|
+
[fileFormatsRecord.JPG]: {
|
|
585
|
+
name: "JPG",
|
|
586
|
+
mimeTypes: ["image/jpeg"],
|
|
587
|
+
extensions: [".jpg", ".jpeg"]
|
|
588
|
+
},
|
|
589
|
+
[fileFormatsRecord.SVG]: {
|
|
590
|
+
name: "SVG",
|
|
591
|
+
mimeTypes: ["image/svg+xml"],
|
|
592
|
+
extensions: [".svg"]
|
|
593
|
+
},
|
|
594
|
+
[fileFormatsRecord.WEBP]: {
|
|
595
|
+
name: "WEBP",
|
|
596
|
+
mimeTypes: ["image/webp"],
|
|
597
|
+
extensions: [".webp"]
|
|
598
|
+
},
|
|
599
|
+
[fileFormatsRecord.AVIF]: {
|
|
600
|
+
name: "AVIF",
|
|
601
|
+
mimeTypes: ["image/avif"],
|
|
602
|
+
extensions: [".avif"]
|
|
603
|
+
},
|
|
604
|
+
[fileFormatsRecord.HEIC]: {
|
|
605
|
+
name: "HEIC",
|
|
606
|
+
mimeTypes: ["image/heic", "image/heif"],
|
|
607
|
+
extensions: [".heic", ".heif"]
|
|
608
|
+
},
|
|
609
|
+
[fileFormatsRecord.PDF]: {
|
|
610
|
+
name: "PDF",
|
|
611
|
+
mimeTypes: ["application/pdf"],
|
|
612
|
+
extensions: [".pdf"]
|
|
613
|
+
},
|
|
614
|
+
[fileFormatsRecord.RTF]: {
|
|
615
|
+
name: "RTF",
|
|
616
|
+
mimeTypes: ["application/rtf"],
|
|
617
|
+
extensions: [".rtf"]
|
|
618
|
+
},
|
|
619
|
+
[fileFormatsRecord.TXT]: {
|
|
620
|
+
name: "TXT",
|
|
621
|
+
mimeTypes: ["text/plain"],
|
|
622
|
+
extensions: [".txt"]
|
|
623
|
+
}
|
|
393
624
|
};
|
|
394
|
-
|
|
395
|
-
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/upload/upload.factory.ts
|
|
627
|
+
/**
|
|
628
|
+
* Defines one shared upload policy while preserving its literal format tuple.
|
|
629
|
+
* The resulting preset can drive schemas, picker hints, and batch validation.
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* const imageUploadPreset = defineUploadPreset({
|
|
633
|
+
* formats: [fileFormat.JPG, fileFormat.PNG, fileFormat.WEBP],
|
|
634
|
+
* maxFileSize: 8 * 1024 * 1024,
|
|
635
|
+
* maxFilesCount: 1,
|
|
636
|
+
* });
|
|
637
|
+
*/
|
|
396
638
|
function defineUploadPreset(preset) {
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
639
|
+
return preset;
|
|
640
|
+
}
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/upload/upload.presets.ts
|
|
643
|
+
/**
|
|
644
|
+
* Default maximum size of one file accepted by the built-in upload presets.
|
|
645
|
+
* The 20 MiB boundary matches the existing upload component policy.
|
|
646
|
+
*/
|
|
647
|
+
const DEFAULT_UPLOAD_MAX_FILE_SIZE = 20971520;
|
|
648
|
+
/**
|
|
649
|
+
* Ready-to-use policy for every image format supported by the upload catalog.
|
|
650
|
+
* Each image may occupy up to 20 MiB while collection capacity remains unrestricted.
|
|
651
|
+
*/
|
|
652
|
+
const imageUploadPreset = defineUploadPreset({
|
|
653
|
+
formats: [
|
|
654
|
+
fileFormat.PNG,
|
|
655
|
+
fileFormat.JPG,
|
|
656
|
+
fileFormat.WEBP,
|
|
657
|
+
fileFormat.AVIF,
|
|
658
|
+
fileFormat.HEIC
|
|
659
|
+
],
|
|
660
|
+
maxFileSize: DEFAULT_UPLOAD_MAX_FILE_SIZE
|
|
405
661
|
});
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
662
|
+
/**
|
|
663
|
+
* Ready-to-use policy for every image format supporting transparency (alpha channel).
|
|
664
|
+
* Each image may occupy up to 20 MiB while collection capacity remains unrestricted.
|
|
665
|
+
*/
|
|
666
|
+
const imageTransparentUploadPreset = defineUploadPreset({
|
|
667
|
+
formats: [
|
|
668
|
+
fileFormat.PNG,
|
|
669
|
+
fileFormat.WEBP,
|
|
670
|
+
fileFormat.SVG
|
|
671
|
+
],
|
|
672
|
+
maxFileSize: DEFAULT_UPLOAD_MAX_FILE_SIZE
|
|
409
673
|
});
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
674
|
+
/**
|
|
675
|
+
* Ready-to-use policy for every document format supported by the upload catalog.
|
|
676
|
+
* Each document may occupy up to 20 MiB while collection capacity remains unrestricted.
|
|
677
|
+
*/
|
|
678
|
+
const documentUploadPreset = defineUploadPreset({
|
|
679
|
+
formats: [
|
|
680
|
+
fileFormat.PDF,
|
|
681
|
+
fileFormat.RTF,
|
|
682
|
+
fileFormat.TXT
|
|
683
|
+
],
|
|
684
|
+
maxFileSize: DEFAULT_UPLOAD_MAX_FILE_SIZE
|
|
413
685
|
});
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region src/upload/upload.utilities.ts
|
|
688
|
+
/**
|
|
689
|
+
* Extracts a normalized trailing extension from a complete file name.
|
|
690
|
+
* Returns an empty string when no valid dot-delimited suffix exists.
|
|
691
|
+
*/
|
|
419
692
|
function getFileExtension(fileName) {
|
|
420
|
-
|
|
421
|
-
|
|
693
|
+
const extensionIndex = fileName.lastIndexOf(".");
|
|
694
|
+
return extensionIndex > 0 && extensionIndex < fileName.length - 1 ? fileName.slice(extensionIndex).toLowerCase() : "";
|
|
422
695
|
}
|
|
696
|
+
/**
|
|
697
|
+
* Normalizes a configured extension to a lowercase dot-prefixed value.
|
|
698
|
+
* Existing prefixes remain intact so repeated normalization is stable.
|
|
699
|
+
*/
|
|
423
700
|
function normalizeFileExtension(extension) {
|
|
424
|
-
|
|
425
|
-
|
|
701
|
+
const normalizedExtension = extension.toLowerCase();
|
|
702
|
+
return normalizedExtension.startsWith(".") ? normalizedExtension : `.${normalizedExtension}`;
|
|
426
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Compares a concrete MIME type with an exact or wildcard configuration.
|
|
706
|
+
* Wildcards match every subtype belonging to the configured media group.
|
|
707
|
+
*/
|
|
427
708
|
function matchesMimeType(fileMimeType, configuredMimeType) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
return fileMimeType === configuredMimeType;
|
|
709
|
+
if (configuredMimeType.endsWith("/*")) return fileMimeType.startsWith(configuredMimeType.slice(0, -1));
|
|
710
|
+
return fileMimeType === configuredMimeType;
|
|
431
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Checks whether a file MIME type belongs to one selected format.
|
|
714
|
+
* File names and extensions cannot make an unsupported MIME type valid.
|
|
715
|
+
*/
|
|
432
716
|
function isFileMimeTypeSupported(file, formats) {
|
|
433
|
-
|
|
717
|
+
return formats.some((format) => fileFormatsConfig[format].mimeTypes.some((mimeType) => matchesMimeType(file.type, mimeType)));
|
|
434
718
|
}
|
|
719
|
+
/**
|
|
720
|
+
* Checks whether a file extension belongs to one selected format.
|
|
721
|
+
* The comparison is case-insensitive and requires a complete suffix.
|
|
722
|
+
*/
|
|
435
723
|
function isFileExtensionSupported(file, formats) {
|
|
436
|
-
|
|
437
|
-
|
|
724
|
+
const fileExtension = getFileExtension(file.name);
|
|
725
|
+
return formats.some((format) => fileFormatsConfig[format].extensions.some((extension) => normalizeFileExtension(extension) === fileExtension));
|
|
438
726
|
}
|
|
727
|
+
/**
|
|
728
|
+
* Checks whether a file matches one configured MIME type or extension.
|
|
729
|
+
* This permissive predicate supports clients where MIME metadata is absent.
|
|
730
|
+
*/
|
|
439
731
|
function isFileFormatSupported(file, formats) {
|
|
440
|
-
|
|
732
|
+
return isFileMimeTypeSupported(file, formats) || isFileExtensionSupported(file, formats);
|
|
441
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* Builds a native file-picker hint from configured MIME types and extensions.
|
|
736
|
+
* Duplicate values are removed while their configuration order is retained.
|
|
737
|
+
*/
|
|
442
738
|
function createUploadAccept(formats) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
739
|
+
const acceptedValues = formats.flatMap((format) => [...fileFormatsConfig[format].mimeTypes, ...fileFormatsConfig[format].extensions]);
|
|
740
|
+
return [...new Set(acceptedValues)].join(",");
|
|
741
|
+
}
|
|
742
|
+
//#endregion
|
|
743
|
+
//#region src/upload/upload.schemas.ts
|
|
744
|
+
/**
|
|
745
|
+
* Builds a reusable Zod schema for one uploaded file with format and size.
|
|
746
|
+
* MIME matching is strict unless extension fallback is explicitly enabled.
|
|
747
|
+
*/
|
|
451
748
|
function zodUploadFileSchema(options) {
|
|
452
|
-
|
|
453
|
-
|
|
749
|
+
const { formats, maxFileSize, extensionFallback = false } = options;
|
|
750
|
+
return z$1.file().min(1, uploadValidationError.EMPTY_FILE).max(maxFileSize, uploadValidationError.FILE_SIZE_EXCEEDED).refine((file) => extensionFallback ? isFileFormatSupported(file, formats) : isFileMimeTypeSupported(file, formats), uploadValidationError.UNSUPPORTED_FILE_FORMAT);
|
|
454
751
|
}
|
|
455
|
-
|
|
456
|
-
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src/upload/upload.validation.ts
|
|
754
|
+
/**
|
|
755
|
+
* Validates one file against configured format and size constraints.
|
|
756
|
+
* Returns the first stable error key or nothing for a valid file.
|
|
757
|
+
*/
|
|
457
758
|
function validateUploadFile(file, formats, maxFileSize) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
return uploadValidationError.UNSUPPORTED_FILE_FORMAT;
|
|
462
|
-
if (file.size > maxFileSize)
|
|
463
|
-
return uploadValidationError.FILE_SIZE_EXCEEDED;
|
|
464
|
-
return void 0;
|
|
759
|
+
if (file.size === 0) return uploadValidationError.EMPTY_FILE;
|
|
760
|
+
if (isFileFormatSupported(file, formats) === false) return uploadValidationError.UNSUPPORTED_FILE_FORMAT;
|
|
761
|
+
if (file.size > maxFileSize) return uploadValidationError.FILE_SIZE_EXCEEDED;
|
|
465
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* Validates an incoming collection while preserving every accepted file.
|
|
765
|
+
* Returns accepted entries and the final rejection key from the batch.
|
|
766
|
+
*/
|
|
466
767
|
function validateUploadFiles(incomingFiles, options) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
768
|
+
const { currentFilesCount, formats, maxFileSize, maxFilesCount } = options;
|
|
769
|
+
const availableFilesCount = maxFilesCount === void 0 ? Infinity : maxFilesCount - currentFilesCount;
|
|
770
|
+
const acceptedFiles = [];
|
|
771
|
+
let validationError;
|
|
772
|
+
for (const file of incomingFiles) {
|
|
773
|
+
const fileValidationError = validateUploadFile(file, formats, maxFileSize) ?? (acceptedFiles.length >= availableFilesCount ? uploadValidationError.FILES_COUNT_EXCEEDED : void 0);
|
|
774
|
+
if (fileValidationError) {
|
|
775
|
+
validationError = fileValidationError;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
acceptedFiles.push(file);
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
acceptedFiles,
|
|
782
|
+
validationError
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/utilities/execution.utilities.ts
|
|
787
|
+
/**
|
|
788
|
+
* Resolves synchronous and asynchronous executions through one promise-based contract.
|
|
789
|
+
* Failures use the supplied fallback or propagate unchanged when none is available.
|
|
790
|
+
*
|
|
791
|
+
* @example
|
|
792
|
+
* await safeExecute(() => fetchData(), (error) => console.error(error));
|
|
793
|
+
*/
|
|
485
794
|
async function safeExecute(fn, onError) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
795
|
+
try {
|
|
796
|
+
return await fn();
|
|
797
|
+
} catch (error) {
|
|
798
|
+
if (onError) return await onError(error);
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
493
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Measures an asynchronous execution while preserving its resolved result.
|
|
804
|
+
* Rejected executions propagate unchanged and do not produce a measurement.
|
|
805
|
+
*
|
|
806
|
+
* @example
|
|
807
|
+
* const { result, executionTime } = await measureExecutionTime(async () => {
|
|
808
|
+
* return await fetchData();
|
|
809
|
+
* });
|
|
810
|
+
* console.log(`Execution time: ${executionTime}ms`);
|
|
811
|
+
*/
|
|
494
812
|
async function measureExecutionTime(execution) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
813
|
+
const startedAt = performance.now();
|
|
814
|
+
const result = await execution();
|
|
815
|
+
const executionTime = performance.now() - startedAt;
|
|
816
|
+
return {
|
|
817
|
+
result,
|
|
818
|
+
executionTime: Math.round(executionTime)
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/zod-bulk/zod-bulk.schemas.ts
|
|
823
|
+
const DEFAULT_ERROR_MESSAGE$1 = "Selection identifiers must be provided.";
|
|
824
|
+
/**
|
|
825
|
+
* Builds a strict selection contract for bulk operations across paginated data.
|
|
826
|
+
* The identifier schema is shared by explicit and all-matching selection modes.
|
|
827
|
+
*
|
|
828
|
+
* `include` targets only identifiers listed by the client. `exclude` targets every
|
|
829
|
+
* item matching the accompanying search snapshot except excluded identifiers.
|
|
830
|
+
*
|
|
831
|
+
* @example
|
|
832
|
+
* const assetBulkSelectionSchema = zodBulkSelectionSchema({
|
|
833
|
+
* identifierSchema: z.string().min(1),
|
|
834
|
+
* });
|
|
835
|
+
*/
|
|
504
836
|
function zodBulkSelectionSchema({ identifierSchema }) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
837
|
+
const identifiersSchema = z$1.array(identifierSchema).refine((identifiers) => new Set(identifiers).size === identifiers.length, { message: DEFAULT_ERROR_MESSAGE$1 });
|
|
838
|
+
return z$1.discriminatedUnion("mode", [z$1.object({
|
|
839
|
+
mode: z$1.literal("include"),
|
|
840
|
+
identifiers: identifiersSchema
|
|
841
|
+
}).strict(), z$1.object({
|
|
842
|
+
mode: z$1.literal("exclude"),
|
|
843
|
+
excludedIdentifiers: identifiersSchema
|
|
844
|
+
}).strict()]);
|
|
845
|
+
}
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/zod-jwt/zod-jwt.services.ts
|
|
848
|
+
/**
|
|
849
|
+
* Signs, decodes, and verifies JWTs with optional Zod payload validation.
|
|
850
|
+
* A supplied schema parses payloads returned by decoding and verification.
|
|
851
|
+
*/
|
|
516
852
|
var ZodJWTService = class {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
853
|
+
payloadSchema;
|
|
854
|
+
algorithm;
|
|
855
|
+
defaultExpirationSeconds;
|
|
856
|
+
constructor(payloadSchema, options) {
|
|
857
|
+
this.payloadSchema = payloadSchema;
|
|
858
|
+
this.algorithm = options?.algorithm ?? "HS256";
|
|
859
|
+
this.defaultExpirationSeconds = options?.defaultExpirationSeconds ?? 900;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Signs a payload using the configured algorithm and expiration settings.
|
|
863
|
+
* Per-call expiration overrides the default configured by the service.
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* const token = await jwtService.sign({ userId: '123' }, secret, { expiresInSeconds: 300 });
|
|
867
|
+
*/
|
|
868
|
+
async sign(payload, secret, options) {
|
|
869
|
+
const exp = Math.floor(Date.now() / 1e3) + (options?.expiresInSeconds ?? this.defaultExpirationSeconds);
|
|
870
|
+
return sign({
|
|
871
|
+
...payload,
|
|
872
|
+
exp
|
|
873
|
+
}, secret, this.algorithm);
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Decodes without authenticating it and parses its payload when a schema exists.
|
|
877
|
+
* Schema validation failures return `null`, while malformed tokens still reject.
|
|
878
|
+
*
|
|
879
|
+
* @example
|
|
880
|
+
* const payload = await jwtService.decode(token);
|
|
881
|
+
*/
|
|
882
|
+
async decode(token) {
|
|
883
|
+
const { payload } = decode(token);
|
|
884
|
+
if (this.payloadSchema === void 0) return payload;
|
|
885
|
+
const { success, data } = await this.payloadSchema.safeParseAsync(payload);
|
|
886
|
+
return success ? data : null;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Verifies a token using the configured algorithm and parses its payload.
|
|
890
|
+
* Signature, expiration, and schema validation failures reject the operation.
|
|
891
|
+
*
|
|
892
|
+
* @example
|
|
893
|
+
* const payload = await jwtService.verifyOrThrow(token, secret);
|
|
894
|
+
*/
|
|
895
|
+
async verifyOrThrow(token, secret) {
|
|
896
|
+
const payload = await verify(token, secret, this.algorithm);
|
|
897
|
+
if (this.payloadSchema === void 0) return payload;
|
|
898
|
+
return this.payloadSchema.parseAsync(payload);
|
|
899
|
+
}
|
|
565
900
|
};
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
901
|
+
//#endregion
|
|
902
|
+
//#region src/zod-search/zod-search.pagination.schemas.ts
|
|
903
|
+
/**
|
|
904
|
+
* Validates offset-based pagination shared by search request contracts.
|
|
905
|
+
* Missing fields inside the required object receive predictable defaults.
|
|
906
|
+
*/
|
|
907
|
+
const zodPaginationSchema = z$1.object({
|
|
908
|
+
/**
|
|
909
|
+
* Zero-based number of records skipped before collecting a result page.
|
|
910
|
+
* String values are coerced to support validation of URL query input.
|
|
911
|
+
*/
|
|
912
|
+
offset: z$1.coerce.number().int().nonnegative().max(1048576).default(0),
|
|
913
|
+
/**
|
|
914
|
+
* Positive maximum number of records returned in a single result page.
|
|
915
|
+
* String values are coerced to support validation of URL query input.
|
|
916
|
+
*/
|
|
917
|
+
limit: z$1.coerce.number().int().positive().max(256).default(10)
|
|
580
918
|
});
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
919
|
+
/**
|
|
920
|
+
* Exposes pagination fields for composition into other object schemas.
|
|
921
|
+
* The shape stays derived from the schema to prevent contract divergence.
|
|
922
|
+
*/
|
|
923
|
+
const zodPaginationShape = zodPaginationSchema.shape;
|
|
924
|
+
//#endregion
|
|
925
|
+
//#region src/zod-validation/zod-validation.refiners.ts
|
|
926
|
+
const DEFAULT_ERROR_MESSAGE = "Invalid input. At least one field must be provided.";
|
|
927
|
+
/**
|
|
928
|
+
* Wraps a partial Zod object schema with:
|
|
929
|
+
* 1. A runtime check ensuring at least one field is non-undefined.
|
|
930
|
+
* 2. A `.transform()` that narrows the output type to `AtLeastOne<T>`,
|
|
931
|
+
* making it directly assignable to domain `*Select` and `*Update` types without casting.
|
|
932
|
+
*
|
|
933
|
+
* @example
|
|
934
|
+
* zQuery(zodAtLeastOne(userSelectSchema)) // result: UserSelect ✓
|
|
935
|
+
*/
|
|
936
|
+
const zodAtLeastOne = (schema) => schema.superRefine((val, ctx) => {
|
|
937
|
+
if (!Object.values(val).some((v) => v !== void 0)) ctx.addIssue({
|
|
938
|
+
code: "custom",
|
|
939
|
+
message: DEFAULT_ERROR_MESSAGE
|
|
940
|
+
});
|
|
592
941
|
}).transform((val) => val);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
942
|
+
//#endregion
|
|
943
|
+
//#region src/zod-search/zod-search.schemas.ts
|
|
944
|
+
/**
|
|
945
|
+
* Optional text query shared by search request contracts.
|
|
946
|
+
* Provided values are trimmed and must contain visible text.
|
|
947
|
+
*/
|
|
948
|
+
const zodSearchQuerySchema = z$1.string().trim().min(1).optional();
|
|
949
|
+
/**
|
|
950
|
+
* Makes every supplied filter optional while requiring one defined value.
|
|
951
|
+
* Top-level optionality is added later for both composition modes equally.
|
|
952
|
+
*/
|
|
953
|
+
const createZodSearchWhereSchema = (filters) => zodAtLeastOne(filters.partial());
|
|
954
|
+
/**
|
|
955
|
+
* Builds a strict schema for reusable search request contracts.
|
|
956
|
+
* The optional `where` object must contain one defined filter.
|
|
957
|
+
*
|
|
958
|
+
* Query is optional and non-empty; pagination is required by default.
|
|
959
|
+
* Literal feature flags update both runtime and inferred static shapes.
|
|
960
|
+
*
|
|
961
|
+
* Pass `filters` for automatic partial and non-empty validation, or use
|
|
962
|
+
* `whereSchema` to preserve a prepared schema with custom Zod effects.
|
|
963
|
+
*
|
|
964
|
+
* @example
|
|
965
|
+
* const assetSearchSchema = zodSearchSchema({
|
|
966
|
+
* filters: assetSchema.pick({ status: true }),
|
|
967
|
+
* });
|
|
968
|
+
*
|
|
969
|
+
* const refinedAssetSearchSchema = zodSearchSchema({
|
|
970
|
+
* whereSchema: zodAtLeastOne(assetSchema.pick({ status: true }).partial()),
|
|
971
|
+
* });
|
|
972
|
+
*/
|
|
973
|
+
const zodSearchSchema = (options) => {
|
|
974
|
+
const shape = {
|
|
975
|
+
where: (options.filters !== void 0 ? createZodSearchWhereSchema(options.filters) : options.whereSchema).optional(),
|
|
976
|
+
...options.queryEnabled !== false ? { query: zodSearchQuerySchema } : {},
|
|
977
|
+
...options.paginationEnabled !== false ? { pagination: zodPaginationSchema } : {}
|
|
978
|
+
};
|
|
979
|
+
return z$1.object(shape).strict();
|
|
605
980
|
};
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
981
|
+
//#endregion
|
|
982
|
+
//#region src/zod-validation/zod-validation.utilities.ts
|
|
983
|
+
/**
|
|
984
|
+
* Checks whether a value is a plain record backed by `Object.prototype`.
|
|
985
|
+
* Arrays, null, dates, collections, and custom class instances are rejected.
|
|
986
|
+
*/
|
|
987
|
+
const isPlainObject = (value) => {
|
|
988
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
613
989
|
};
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return value;
|
|
627
|
-
if (normalized === "true")
|
|
628
|
-
return true;
|
|
629
|
-
if (normalized === "false")
|
|
630
|
-
return false;
|
|
631
|
-
if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized))
|
|
632
|
-
return Number(normalized);
|
|
633
|
-
return value;
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/zod-validation/zod-validation.parsing.ts
|
|
992
|
+
const parseQueryValue = (value) => {
|
|
993
|
+
if (Array.isArray(value)) return value.map(parseQueryValue);
|
|
994
|
+
if (isPlainObject(value)) return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, parseQueryValue(entryValue)]));
|
|
995
|
+
if (typeof value !== "string") return value;
|
|
996
|
+
const normalized = value.trim().toLowerCase();
|
|
997
|
+
if (normalized === "") return value;
|
|
998
|
+
if (normalized === "true") return true;
|
|
999
|
+
if (normalized === "false") return false;
|
|
1000
|
+
if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized)) return Number(normalized);
|
|
1001
|
+
return value;
|
|
634
1002
|
};
|
|
635
|
-
|
|
1003
|
+
/**
|
|
1004
|
+
* Preprocesses query-like values before Zod validation.
|
|
1005
|
+
*
|
|
1006
|
+
* Query parameters are string-based by nature, even when they semantically represent
|
|
1007
|
+
* numbers or booleans. This helper converts only clear primitive values, allowing
|
|
1008
|
+
* regular schemas like `z.number()` and `z.boolean()` to validate query input directly.
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* const schema = asQuery(z.object({
|
|
1012
|
+
* page: z.number().int().positive(),
|
|
1013
|
+
* isActive: z.boolean(),
|
|
1014
|
+
* }));
|
|
1015
|
+
*
|
|
1016
|
+
* schema.parse({ page: '2', isActive: 'true' });
|
|
1017
|
+
* // { page: 2, isActive: true }
|
|
1018
|
+
*/
|
|
1019
|
+
const asQuery = (schema) => z$1.preprocess(parseQueryValue, schema);
|
|
1020
|
+
/**
|
|
1021
|
+
* Recursively converts payload values to strings without changing container structure.
|
|
1022
|
+
* Objects and arrays are copied while `null` and `undefined` retain omission semantics.
|
|
1023
|
+
*
|
|
1024
|
+
* @example
|
|
1025
|
+
* stringifyPayloadValues({ page: 2, enabled: true });
|
|
1026
|
+
* // { page: '2', enabled: 'true' }
|
|
1027
|
+
*/
|
|
636
1028
|
function stringifyPayloadValues(value) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
export {
|
|
651
|
-
DEFAULT_HMAC_ALGORITHM,
|
|
652
|
-
DEFAULT_HMAC_ENCODING,
|
|
653
|
-
DEFAULT_UPLOAD_MAX_FILE_SIZE,
|
|
654
|
-
EXCEPTION_STATUS_CODES,
|
|
655
|
-
HMACService,
|
|
656
|
-
REDACTED_LOG_VALUE,
|
|
657
|
-
SUCCESS_STATUS_CODES,
|
|
658
|
-
ZodJWTService,
|
|
659
|
-
asQuery,
|
|
660
|
-
createStringEnumRecord,
|
|
661
|
-
createUploadAccept,
|
|
662
|
-
createZodSearchWhereSchema,
|
|
663
|
-
decodeBase64,
|
|
664
|
-
decodeHMACSignature,
|
|
665
|
-
defineUploadPreset,
|
|
666
|
-
documentUploadPreset,
|
|
667
|
-
encodeBase64,
|
|
668
|
-
encodeHMACSignature,
|
|
669
|
-
failure,
|
|
670
|
-
fetchAndThrow,
|
|
671
|
-
fetchSafely,
|
|
672
|
-
fileFormat,
|
|
673
|
-
fileFormatsArray,
|
|
674
|
-
fileFormatsConfig,
|
|
675
|
-
fileFormatsRecord,
|
|
676
|
-
fileRespond,
|
|
677
|
-
formatTime,
|
|
678
|
-
generateRandomString,
|
|
679
|
-
getColoredHTTPStatus,
|
|
680
|
-
getFileExtension,
|
|
681
|
-
getFormattedDate,
|
|
682
|
-
getFormattedTime,
|
|
683
|
-
getUTCOffset,
|
|
684
|
-
getZonedTime,
|
|
685
|
-
hmacAlgorithm,
|
|
686
|
-
hmacAlgorithmsArray,
|
|
687
|
-
hmacAlgorithmsRecord,
|
|
688
|
-
hmacEncoding,
|
|
689
|
-
hmacEncodingsArray,
|
|
690
|
-
hmacEncodingsRecord,
|
|
691
|
-
httpStatusColors,
|
|
692
|
-
imageTransparentUploadPreset,
|
|
693
|
-
imageUploadPreset,
|
|
694
|
-
isFileExtensionSupported,
|
|
695
|
-
isFileFormatSupported,
|
|
696
|
-
isFileMimeTypeSupported,
|
|
697
|
-
isPlainObject,
|
|
698
|
-
log,
|
|
699
|
-
logLevel,
|
|
700
|
-
logLevelColors,
|
|
701
|
-
logLevelsArray,
|
|
702
|
-
logLevelsRecord,
|
|
703
|
-
loggingMiddleware,
|
|
704
|
-
matchesMimeType,
|
|
705
|
-
measureExecutionTime,
|
|
706
|
-
normalizeFileExtension,
|
|
707
|
-
onHandlerError,
|
|
708
|
-
parseQueryValue,
|
|
709
|
-
redactSensitiveJSON,
|
|
710
|
-
redactSensitiveSearchParams,
|
|
711
|
-
respond,
|
|
712
|
-
safeExecute,
|
|
713
|
-
sensitiveLogKeyParts,
|
|
714
|
-
sqlWhere,
|
|
715
|
-
stringifyPayloadValues,
|
|
716
|
-
success,
|
|
717
|
-
toHMACBytes,
|
|
718
|
-
uploadValidationError,
|
|
719
|
-
uploadValidationErrorsArray,
|
|
720
|
-
uploadValidationErrorsRecord,
|
|
721
|
-
validateUploadFile,
|
|
722
|
-
validateUploadFiles,
|
|
723
|
-
zodAtLeastOne,
|
|
724
|
-
zodBulkSelectionSchema,
|
|
725
|
-
zodPaginationSchema,
|
|
726
|
-
zodPaginationShape,
|
|
727
|
-
zodSearchQuerySchema,
|
|
728
|
-
zodSearchSchema,
|
|
729
|
-
zodUploadFileSchema
|
|
730
|
-
};
|
|
1029
|
+
if (Array.isArray(value)) return value.map(stringifyPayloadValues);
|
|
1030
|
+
if (isPlainObject(value)) {
|
|
1031
|
+
const entries = Object.entries(value).map(([key, entryValue]) => {
|
|
1032
|
+
return [key, stringifyPayloadValues(entryValue)];
|
|
1033
|
+
});
|
|
1034
|
+
return Object.fromEntries(entries);
|
|
1035
|
+
}
|
|
1036
|
+
if (value === null || value === void 0) return value;
|
|
1037
|
+
return String(value);
|
|
1038
|
+
}
|
|
1039
|
+
//#endregion
|
|
1040
|
+
export { DEFAULT_HMAC_ALGORITHM, DEFAULT_HMAC_ENCODING, DEFAULT_UPLOAD_MAX_FILE_SIZE, EXCEPTION_STATUS_CODES, HMACService, REDACTED_LOG_VALUE, SUCCESS_STATUS_CODES, ZodJWTService, asQuery, createStringEnumRecord, createUploadAccept, createZodSearchWhereSchema, decodeBase64, decodeHMACSignature, defineUploadPreset, documentUploadPreset, encodeBase64, encodeHMACSignature, failure, fetchAndThrow, fetchSafely, fileFormat, fileFormatsArray, fileFormatsConfig, fileFormatsRecord, fileRespond, formatTime, generateRandomString, getColoredHTTPStatus, getFileExtension, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, hmacAlgorithm, hmacAlgorithmsArray, hmacAlgorithmsRecord, hmacEncoding, hmacEncodingsArray, hmacEncodingsRecord, httpStatusColors, imageTransparentUploadPreset, imageUploadPreset, isFileExtensionSupported, isFileFormatSupported, isFileMimeTypeSupported, isPlainObject, log, logLevel, logLevelColors, logLevelsArray, logLevelsRecord, loggingMiddleware, matchesMimeType, measureExecutionTime, normalizeFileExtension, onHandlerError, parseQueryValue, redactSensitiveJSON, redactSensitiveSearchParams, respond, safeExecute, sensitiveLogKeyParts, sqlWhere, stringifyPayloadValues, success, toHMACBytes, uploadValidationError, uploadValidationErrorsArray, uploadValidationErrorsRecord, validateUploadFile, validateUploadFiles, zodAtLeastOne, zodBulkSelectionSchema, zodPaginationSchema, zodPaginationShape, zodSearchQuerySchema, zodSearchSchema, zodUploadFileSchema };
|