@kalutskii/foundation 0.7.19 → 0.7.22

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/README.md CHANGED
@@ -29,6 +29,7 @@ The package contains several deliberately isolated areas:
29
29
  | `zod-search` | Reusable search and pagination contracts composed from lower-level Zod primitives. |
30
30
  | `zod-bulk` | Include/exclude selection contracts shared by frontend and backend bulk operations. |
31
31
  | `hono` | Hono-specific response, file response, error handling, and request logging adapters. |
32
+ | `hmac` | Web Crypto HMAC signing, verification, algorithms, and signature encoding. |
32
33
  | `drizzle` | Drizzle-specific SQL composition that must not leak into generic contract modules. |
33
34
  | `zod-jwt` | JWT service integration with optional Zod validation of decoded payloads. |
34
35
 
@@ -87,6 +88,7 @@ Otherwise, create a dedicated module instead of growing an unrelated file with a
87
88
  ```text
88
89
  src/
89
90
  ├── drizzle/
91
+ ├── hmac/
90
92
  ├── hono/
91
93
  ├── http/
92
94
  ├── upload/
@@ -310,7 +312,7 @@ bun install --frozen-lockfile
310
312
  ```bash
311
313
  bun test src/<module>/<module>.spec.ts
312
314
  bun run typecheck
313
- bunx eslint src/<module>
315
+ bunx oxlint src/<module>
314
316
  ```
315
317
 
316
318
  Run focused checks first to shorten feedback loops. Do not claim a command passed unless it was actually executed
package/dist/index.d.ts CHANGED
@@ -76,6 +76,107 @@ declare function respond<T extends object = Record<string, never>, S extends Suc
76
76
  */
77
77
  declare function fileRespond<S extends SuccessStatusCode>(c: Context, options: HonoFileRespondOptions<S>): Response;
78
78
 
79
+ /**
80
+ * Default digest algorithm used by `HMACService` when none is configured.
81
+ * SHA-256 provides broad Web Crypto support and a 256-bit authentication tag.
82
+ */
83
+ declare const DEFAULT_HMAC_ALGORITHM: "SHA-256";
84
+ /**
85
+ * Default textual encoding used for signatures returned by `HMACService`.
86
+ * Hex output remains deterministic, portable, and independent of padding rules.
87
+ */
88
+ declare const DEFAULT_HMAC_ENCODING: "hex";
89
+
90
+ declare const hmacAlgorithmsArray: readonly ["SHA-256", "SHA-384", "SHA-512"];
91
+ type HMACAlgorithm = (typeof hmacAlgorithmsArray)[number];
92
+ declare const hmacAlgorithmsRecord: Readonly<{
93
+ SHA_256: "SHA-256";
94
+ SHA_384: "SHA-384";
95
+ SHA_512: "SHA-512";
96
+ }>;
97
+ declare const hmacAlgorithm: Readonly<{
98
+ SHA_256: "SHA-256";
99
+ SHA_384: "SHA-384";
100
+ SHA_512: "SHA-512";
101
+ }>;
102
+ declare const hmacEncodingsArray: readonly ["hex", "base64", "base64url"];
103
+ type HMACEncoding = (typeof hmacEncodingsArray)[number];
104
+ declare const hmacEncodingsRecord: Readonly<{
105
+ HEX: "hex";
106
+ BASE64: "base64";
107
+ BASE64URL: "base64url";
108
+ }>;
109
+ declare const hmacEncoding: Readonly<{
110
+ HEX: "hex";
111
+ BASE64: "base64";
112
+ BASE64URL: "base64url";
113
+ }>;
114
+
115
+ /**
116
+ * Binary-safe input accepted as either an HMAC payload or a secret key.
117
+ * Strings use UTF-8 encoding while byte arrays preserve their exact contents.
118
+ */
119
+ type HMACInput = string | Uint8Array;
120
+ /**
121
+ * Configures the digest algorithm and textual signature encoding for one service.
122
+ * Omitted values select SHA-256 and lowercase hexadecimal output by default.
123
+ */
124
+ type HMACServiceOptions = Readonly<{
125
+ /**
126
+ * Web Crypto digest algorithm used to authenticate every payload.
127
+ * The service defaults to `SHA-256` when this option is omitted.
128
+ */
129
+ algorithm?: HMACAlgorithm;
130
+ /**
131
+ * Text encoding applied to generated and verified signatures.
132
+ * The service defaults to `hex` when this option is omitted.
133
+ */
134
+ encoding?: HMACEncoding;
135
+ }>;
136
+
137
+ /**
138
+ * Creates and verifies keyed message authentication codes through Web Crypto.
139
+ * Each instance preserves its digest algorithm and textual encoding configuration.
140
+ */
141
+ declare class HMACService {
142
+ readonly algorithm: HMACAlgorithm;
143
+ readonly encoding: HMACEncoding;
144
+ constructor(options?: HMACServiceOptions);
145
+ /**
146
+ * Authenticates a payload with a secret and returns the encoded signature.
147
+ * String inputs use UTF-8 while byte arrays preserve their exact byte sequence.
148
+ *
149
+ * @example
150
+ * const signature = await hmacService.sign('payload', 'shared-secret');
151
+ */
152
+ sign(payload: HMACInput, secret: HMACInput): Promise<string>;
153
+ /**
154
+ * Verifies an encoded signature without requiring a manual equality comparison.
155
+ * Invalid encodings, incorrect secrets, and altered payloads all resolve to `false`.
156
+ *
157
+ * @example
158
+ * const verified = await hmacService.verify('payload', signature, 'shared-secret');
159
+ */
160
+ verify(payload: HMACInput, signature: string, secret: HMACInput): Promise<boolean>;
161
+ private importSecret;
162
+ }
163
+
164
+ /**
165
+ * Converts an HMAC payload or secret into an isolated byte array representation.
166
+ * Strings use UTF-8 while supplied bytes are copied to prevent later mutation.
167
+ */
168
+ declare function toHMACBytes(input: HMACInput): Uint8Array<ArrayBuffer>;
169
+ /**
170
+ * Encodes binary HMAC output with one supported transport-safe representation.
171
+ * Hex output remains lowercase while Base64 URL output omits conventional padding.
172
+ */
173
+ declare function encodeHMACSignature(signature: Uint8Array, encoding: HMACEncoding): string;
174
+ /**
175
+ * Decodes a textual HMAC signature into bytes for cryptographic verification.
176
+ * Malformed alphabets, lengths, or padding combinations reject with `TypeError`.
177
+ */
178
+ declare function decodeHMACSignature(signature: string, encoding: HMACEncoding): Uint8Array<ArrayBuffer>;
179
+
79
180
  /**
80
181
  * Creates a successful API envelope without cloning its data.
81
182
  * Generic inference preserves the exact supplied payload type.
@@ -531,6 +632,17 @@ declare function formatTime(time: Date, { locale, tz }?: {
531
632
  tz?: string;
532
633
  }): string;
533
634
 
635
+ /**
636
+ * Encodes arbitrary binary bytes into their canonical Base64 representation.
637
+ * Chunked conversion avoids the argument limit imposed by `String.fromCharCode`.
638
+ */
639
+ declare function encodeBase64(bytes: Uint8Array): string;
640
+ /**
641
+ * Decodes a canonical or padded Base64 string into its original binary bytes.
642
+ * Invalid input preserves the native `atob` failure instead of returning partial data.
643
+ */
644
+ declare function decodeBase64(value: string): Uint8Array<ArrayBuffer>;
645
+
534
646
  /**
535
647
  * Recursively replaces dots in a string literal with underscores.
536
648
  * Every other character remains unchanged in the resulting literal type.
@@ -923,4 +1035,4 @@ declare function stringifyPayloadValues<TValue>(value: TValue): StringifiedPaylo
923
1035
  */
924
1036
  declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
925
1037
 
926
- export { type APIContractData, type APIContractError, type APIContractResult, type APIError, type APISuccess, type AtLeastOne, DEFAULT_UPLOAD_MAX_FILE_SIZE, EXCEPTION_STATUS_CODES, type ExceptionStatusCode, type FetchResult, type FileFormat, type FileFormatConfig, type HonoFileRespondOptions, type HonoRespondOptions, type JWTServiceOptions, type JWTSignOptions, type LogLevel, type MeasuredExecution, type Payload, type PayloadSchema, REDACTED_LOG_VALUE, type ReplaceDotsWithUnderscores, type ReplaceHyphensWithUnderscores, SUCCESS_STATUS_CODES, type Simplify, type StringEnumRecord, type StringifiedPayload, type SuccessStatusCode, type UploadFilesValidationOptions, type UploadFilesValidationResult, type UploadPreset, type UploadValidationError, type ZodBulkExcludeSelection, type ZodBulkIncludeSelection, type ZodBulkSelection, ZodJWTService, type ZodPaginationOptions, type ZodSearchSchemaOptions, type ZodUploadFileSchemaOptions, asQuery, createStringEnumRecord, createUploadAccept, createZodSearchWhereSchema, defineUploadPreset, documentUploadPreset, failure, fetchAndThrow, fetchSafely, fileFormat, fileFormatsArray, fileFormatsConfig, fileFormatsRecord, fileRespond, formatTime, generateRandomString, getColoredHTTPStatus, getFileExtension, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, 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, uploadValidationError, uploadValidationErrorsArray, uploadValidationErrorsRecord, validateUploadFile, validateUploadFiles, zodAtLeastOne, zodBulkSelectionSchema, zodPaginationSchema, zodPaginationShape, zodSearchQuerySchema, zodSearchSchema, zodUploadFileSchema };
1038
+ export { type APIContractData, type APIContractError, type APIContractResult, type APIError, type APISuccess, type AtLeastOne, DEFAULT_HMAC_ALGORITHM, DEFAULT_HMAC_ENCODING, DEFAULT_UPLOAD_MAX_FILE_SIZE, EXCEPTION_STATUS_CODES, type ExceptionStatusCode, type FetchResult, type FileFormat, type FileFormatConfig, type HMACAlgorithm, type HMACEncoding, type HMACInput, HMACService, type HMACServiceOptions, type HonoFileRespondOptions, type HonoRespondOptions, type JWTServiceOptions, type JWTSignOptions, type LogLevel, type MeasuredExecution, type Payload, type PayloadSchema, REDACTED_LOG_VALUE, type ReplaceDotsWithUnderscores, type ReplaceHyphensWithUnderscores, SUCCESS_STATUS_CODES, type Simplify, type StringEnumRecord, type StringifiedPayload, type SuccessStatusCode, type UploadFilesValidationOptions, type UploadFilesValidationResult, type UploadPreset, type UploadValidationError, type ZodBulkExcludeSelection, type ZodBulkIncludeSelection, type ZodBulkSelection, ZodJWTService, type ZodPaginationOptions, type ZodSearchSchemaOptions, type ZodUploadFileSchemaOptions, 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 };
package/dist/index.js CHANGED
@@ -156,6 +156,115 @@ function fileRespond(c, options) {
156
156
  return c.body(options.content, options.status);
157
157
  }
158
158
 
159
+ // src/hmac/hmac.enums.ts
160
+ var hmacAlgorithmsArray = ["SHA-256", "SHA-384", "SHA-512"];
161
+ var hmacAlgorithmsRecord = createStringEnumRecord(hmacAlgorithmsArray);
162
+ var hmacAlgorithm = hmacAlgorithmsRecord;
163
+ var hmacEncodingsArray = ["hex", "base64", "base64url"];
164
+ var hmacEncodingsRecord = createStringEnumRecord(hmacEncodingsArray);
165
+ var hmacEncoding = hmacEncodingsRecord;
166
+
167
+ // src/hmac/hmac.constants.ts
168
+ var DEFAULT_HMAC_ALGORITHM = hmacAlgorithm.SHA_256;
169
+ var DEFAULT_HMAC_ENCODING = hmacEncoding.HEX;
170
+
171
+ // src/utilities/encoding.utilities.ts
172
+ var BASE64_BYTE_CHUNK_SIZE = 32768;
173
+ function encodeBase64(bytes) {
174
+ let binary = "";
175
+ for (let offset = 0; offset < bytes.length; offset += BASE64_BYTE_CHUNK_SIZE) {
176
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_BYTE_CHUNK_SIZE));
177
+ }
178
+ return btoa(binary);
179
+ }
180
+ function decodeBase64(value) {
181
+ const binary = atob(value);
182
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
183
+ }
184
+
185
+ // src/hmac/hmac.utilities.ts
186
+ var textEncoder = new TextEncoder();
187
+ function toHMACBytes(input) {
188
+ if (typeof input === "string")
189
+ return textEncoder.encode(input);
190
+ const bytes = new Uint8Array(input.length);
191
+ bytes.set(input);
192
+ return bytes;
193
+ }
194
+ function encodeHMACSignature(signature, encoding) {
195
+ if (encoding === "hex") {
196
+ return Array.from(signature, (byte) => byte.toString(16).padStart(2, "0")).join("");
197
+ }
198
+ const base64 = encodeBase64(signature);
199
+ if (encoding === "base64")
200
+ return base64;
201
+ return base64.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
202
+ }
203
+ function decodeHMACSignature(signature, encoding) {
204
+ if (encoding === "hex") {
205
+ if (signature.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(signature)) {
206
+ throw new TypeError("Invalid hexadecimal HMAC signature");
207
+ }
208
+ return Uint8Array.from(signature.match(/.{2}/g) ?? [], (byte) => Number.parseInt(byte, 16));
209
+ }
210
+ if (encoding === "base64") {
211
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(signature)) {
212
+ throw new TypeError("Invalid Base64 HMAC signature");
213
+ }
214
+ return decodeBase64(signature);
215
+ }
216
+ if (!/^[A-Za-z0-9_-]*$/.test(signature) || signature.length % 4 === 1) {
217
+ throw new TypeError("Invalid Base64 URL HMAC signature");
218
+ }
219
+ const base64 = signature.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(signature.length / 4) * 4, "=");
220
+ return decodeBase64(base64);
221
+ }
222
+
223
+ // src/hmac/hmac.services.ts
224
+ var HMACService = class {
225
+ algorithm;
226
+ encoding;
227
+ constructor(options = {}) {
228
+ this.algorithm = options.algorithm ?? DEFAULT_HMAC_ALGORITHM;
229
+ this.encoding = options.encoding ?? DEFAULT_HMAC_ENCODING;
230
+ }
231
+ /**
232
+ * Authenticates a payload with a secret and returns the encoded signature.
233
+ * String inputs use UTF-8 while byte arrays preserve their exact byte sequence.
234
+ *
235
+ * @example
236
+ * const signature = await hmacService.sign('payload', 'shared-secret');
237
+ */
238
+ async sign(payload, secret) {
239
+ const key = await this.importSecret(secret);
240
+ const signature = await crypto.subtle.sign("HMAC", key, toHMACBytes(payload));
241
+ return encodeHMACSignature(new Uint8Array(signature), this.encoding);
242
+ }
243
+ /**
244
+ * Verifies an encoded signature without requiring a manual equality comparison.
245
+ * Invalid encodings, incorrect secrets, and altered payloads all resolve to `false`.
246
+ *
247
+ * @example
248
+ * const verified = await hmacService.verify('payload', signature, 'shared-secret');
249
+ */
250
+ async verify(payload, signature, secret) {
251
+ let signatureBytes;
252
+ try {
253
+ signatureBytes = decodeHMACSignature(signature, this.encoding);
254
+ } catch {
255
+ return false;
256
+ }
257
+ const key = await this.importSecret(secret);
258
+ return crypto.subtle.verify("HMAC", key, signatureBytes, toHMACBytes(payload));
259
+ }
260
+ async importSecret(secret) {
261
+ const secretBytes = toHMACBytes(secret);
262
+ const algorithm = { name: "HMAC", hash: this.algorithm };
263
+ const keyUsages = ["sign", "verify"];
264
+ return crypto.subtle.importKey("raw", secretBytes, algorithm, false, keyUsages);
265
+ }
266
+ };
267
+
159
268
  // src/http/http.constants.ts
160
269
  var SUCCESS_STATUS_CODES = [200, 201, 202, 307];
161
270
  var EXCEPTION_STATUS_CODES = [400, 401, 403, 404, 405, 409, 500];
@@ -508,7 +617,7 @@ var parseQueryValue = (value) => {
508
617
  if (Array.isArray(value))
509
618
  return value.map(parseQueryValue);
510
619
  if (isPlainObject(value)) {
511
- return Object.fromEntries(Object.entries(value).map(([key, value2]) => [key, parseQueryValue(value2)]));
620
+ return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, parseQueryValue(entryValue)]));
512
621
  }
513
622
  if (typeof value !== "string")
514
623
  return value;
@@ -539,8 +648,11 @@ function stringifyPayloadValues(value) {
539
648
  return String(value);
540
649
  }
541
650
  export {
651
+ DEFAULT_HMAC_ALGORITHM,
652
+ DEFAULT_HMAC_ENCODING,
542
653
  DEFAULT_UPLOAD_MAX_FILE_SIZE,
543
654
  EXCEPTION_STATUS_CODES,
655
+ HMACService,
544
656
  REDACTED_LOG_VALUE,
545
657
  SUCCESS_STATUS_CODES,
546
658
  ZodJWTService,
@@ -548,8 +660,12 @@ export {
548
660
  createStringEnumRecord,
549
661
  createUploadAccept,
550
662
  createZodSearchWhereSchema,
663
+ decodeBase64,
664
+ decodeHMACSignature,
551
665
  defineUploadPreset,
552
666
  documentUploadPreset,
667
+ encodeBase64,
668
+ encodeHMACSignature,
553
669
  failure,
554
670
  fetchAndThrow,
555
671
  fetchSafely,
@@ -566,6 +682,12 @@ export {
566
682
  getFormattedTime,
567
683
  getUTCOffset,
568
684
  getZonedTime,
685
+ hmacAlgorithm,
686
+ hmacAlgorithmsArray,
687
+ hmacAlgorithmsRecord,
688
+ hmacEncoding,
689
+ hmacEncodingsArray,
690
+ hmacEncodingsRecord,
569
691
  httpStatusColors,
570
692
  imageTransparentUploadPreset,
571
693
  imageUploadPreset,
@@ -592,6 +714,7 @@ export {
592
714
  sqlWhere,
593
715
  stringifyPayloadValues,
594
716
  success,
717
+ toHMACBytes,
595
718
  uploadValidationError,
596
719
  uploadValidationErrorsArray,
597
720
  uploadValidationErrorsRecord,
package/package.json CHANGED
@@ -1,13 +1,22 @@
1
1
  {
2
2
  "name": "@kalutskii/foundation",
3
- "version": "0.7.19",
3
+ "version": "0.7.22",
4
4
  "description": "Typescript collection of most common utilities, schemas and functions among private projects.",
5
5
  "license": "MIT",
6
- "type": "module",
7
6
  "repository": {
8
7
  "type": "git",
9
8
  "url": "git+https://github.com/kalutskii/foundation.git"
10
9
  },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "type": "module",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./dist/index.js",
17
+ "types": "./dist/index.d.ts"
18
+ }
19
+ },
11
20
  "publishConfig": {
12
21
  "access": "public"
13
22
  },
@@ -15,20 +24,20 @@
15
24
  "build": "tsup",
16
25
  "test": "bun test",
17
26
  "typecheck": "tsc --noEmit",
18
- "lint": "eslint . --ext .ts --fix",
19
- "lint:check": "eslint . --ext .ts",
20
- "format": "prettier --write .",
21
- "format:check": "prettier --check ."
27
+ "lint": "oxlint",
28
+ "lint:check": "oxlint --fix",
29
+ "format": "oxfmt",
30
+ "format:check": "oxfmt --check"
22
31
  },
23
- "exports": {
24
- ".": {
25
- "import": "./dist/index.js",
26
- "types": "./dist/index.d.ts"
27
- }
32
+ "devDependencies": {
33
+ "@types/bun": "latest",
34
+ "esbuild-plugin-tsconfig-paths": "^1.0.2",
35
+ "oxfmt": "^0.63.0",
36
+ "oxlint": "^1.78.0",
37
+ "oxlint-tsgolint": "^7.0.2001",
38
+ "tsup": "^8.5.1",
39
+ "typescript": "^5.9.3"
28
40
  },
29
- "files": [
30
- "dist"
31
- ],
32
41
  "peerDependencies": {
33
42
  "date-fns": "^4.1.0",
34
43
  "date-fns-tz": "^3.2.0",
@@ -37,17 +46,5 @@
37
46
  "kleur": "^4.1.5",
38
47
  "typescript": "^5",
39
48
  "zod": "^4.4.3"
40
- },
41
- "devDependencies": {
42
- "@eslint/js": "^10.0.1",
43
- "@trivago/prettier-plugin-sort-imports": "^6.0.2",
44
- "@types/bun": "latest",
45
- "esbuild-plugin-tsconfig-paths": "^1.0.2",
46
- "eslint": "^10.2.1",
47
- "eslint-config-prettier": "^10.1.8",
48
- "jiti": "^2.7.0",
49
- "prettier": "^3.8.1",
50
- "tsup": "^8.5.1",
51
- "typescript-eslint": "^8.58.2"
52
49
  }
53
50
  }