@zudojs/types 0.1.0 → 1.0.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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -6
  3. package/dist/runtime/index.d.ts +9 -1
  4. package/dist/runtime/index.js +8 -1
  5. package/dist/runtime/runtime.core.d.ts +57 -4
  6. package/dist/runtime/runtime.core.js +70 -32
  7. package/dist/typeConverters/typeConverters.core.d.ts +33 -3
  8. package/dist/typeConverters/typeConverters.core.js +99 -20
  9. package/dist/typeGuards/index.d.ts +1 -1
  10. package/dist/typeGuards/index.js +1 -1
  11. package/dist/typeGuards/typeGuards.core.d.ts +51 -4
  12. package/dist/typeGuards/typeGuards.core.js +110 -13
  13. package/dist/typeUtilities/index.d.ts +1 -1
  14. package/dist/typeUtilities/typeUtilities.core.d.ts +22 -3
  15. package/package.json +19 -12
  16. package/dist/.tsbuildinfo +0 -1
  17. package/dist/index.d.ts.map +0 -1
  18. package/dist/index.js.map +0 -1
  19. package/dist/runtime/index.d.ts.map +0 -1
  20. package/dist/runtime/index.js.map +0 -1
  21. package/dist/runtime/runtime.core.d.ts.map +0 -1
  22. package/dist/runtime/runtime.core.js.map +0 -1
  23. package/dist/typeConverters/index.d.ts.map +0 -1
  24. package/dist/typeConverters/index.js.map +0 -1
  25. package/dist/typeConverters/typeConverters.core.d.ts.map +0 -1
  26. package/dist/typeConverters/typeConverters.core.js.map +0 -1
  27. package/dist/typeGuards/index.d.ts.map +0 -1
  28. package/dist/typeGuards/index.js.map +0 -1
  29. package/dist/typeGuards/typeGuards.core.d.ts.map +0 -1
  30. package/dist/typeGuards/typeGuards.core.js.map +0 -1
  31. package/dist/typeUtilities/index.d.ts.map +0 -1
  32. package/dist/typeUtilities/index.js.map +0 -1
  33. package/dist/typeUtilities/typeUtilities.core.d.ts.map +0 -1
  34. package/dist/typeUtilities/typeUtilities.core.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zudojs Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -13,26 +13,64 @@ npm install @zudojs/types
13
13
  ```typescript
14
14
  import {
15
15
  isPlainObject,
16
- isDate,
17
16
  isEmail,
18
- Maybe,
19
- DeepReadonly,
17
+ isUuid,
18
+ systemClock,
19
+ systemRandom,
20
+ toNumber,
20
21
  } from "@zudojs/types";
22
+ import type { Maybe, DeepReadonly } from "@zudojs/types";
21
23
 
22
24
  if (isPlainObject(value)) {
23
- console.log(value.keys());
25
+ for (const key of Object.keys(value)) {
26
+ console.log(key, value[key]);
27
+ }
24
28
  }
25
29
 
26
30
  const id: Maybe<string> = null;
27
31
  const config: DeepReadonly<AppConfig> = { db: { host: "localhost" } };
32
+
33
+ // Injectable runtime primitives, so tests can substitute deterministic ones.
34
+ const token = systemRandom.string(32);
35
+ const now = systemClock.now();
36
+
37
+ // Converters refuse rather than guessing: "" and "0x10" fall back.
38
+ const limit = toNumber(query.limit, 20);
39
+ ```
40
+
41
+ Deterministic doubles for tests:
42
+
43
+ ```typescript
44
+ import { FixedClock, SeededRandom } from "@zudojs/types";
45
+ import type { PseudoRandom } from "@zudojs/types";
46
+
47
+ const clock = new FixedClock(0);
48
+ const random: PseudoRandom = new SeededRandom(42);
49
+
50
+ clock.advance(1_000);
28
51
  ```
29
52
 
53
+ ## Safety Notes
54
+
55
+ - `Random` is branded, so `SeededRandom` — whose output is fully predictable
56
+ from its seed — cannot be injected where unpredictability is required.
57
+ Implement a secure generator through `defineSecureRandom()`.
58
+ - `Random.int()` uses rejection sampling, not `% max`, so draws are uniform for
59
+ every bound rather than only powers of two.
60
+ - `mapToObject` and `safeJsonParse` cannot be used to reach a prototype.
61
+ - `toNumber` requires a finite number and refuses blank strings, hexadecimal
62
+ literals and `1e999`; `toBoolean(NaN)` falls back rather than returning true.
63
+ - `isUuid` accepts versions 1–8 including UUIDv7; use `isUuidV4` where the
64
+ version matters. `isPromise` narrows only to a native `Promise` — use
65
+ `isThenable` for anything awaitable.
66
+
30
67
  ## Features
31
68
 
32
- - Type guards (`isPlainObject`, `isDate`, `isEmail`, etc.)
69
+ - Type guards (`isPlainObject`, `isDate`, `isEmail`, `isUuid`, etc.)
33
70
  - Utility types (`Maybe`, `DeepReadonly`, `Prettify`, etc.)
34
- - Type converters and transformers
71
+ - Type converters and case transformers
35
72
  - Branded type utilities
73
+ - Injectable `Clock` and `Random` primitives, with deterministic test doubles
36
74
 
37
75
  ## Use Cases
38
76
 
@@ -1,2 +1,10 @@
1
- export * from "./runtime.core.js";
1
+ /**
2
+ * @zudojs/types — Runtime primitives barrel.
3
+ *
4
+ * An explicit list rather than `export *`: this was the only barrel in the
5
+ * package without one, so anything newly exported from `runtime.core.ts`
6
+ * silently became public API.
7
+ */
8
+ export type { Clock, ClockSeconds, Random, PseudoRandom, } from "./runtime.core.js";
9
+ export { systemClock, systemClockSeconds, defineSecureRandom, systemRandom, FixedClock, SeededRandom, } from "./runtime.core.js";
2
10
  //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1,9 @@
1
- export * from "./runtime.core.js";
1
+ /**
2
+ * @zudojs/types — Runtime primitives barrel.
3
+ *
4
+ * An explicit list rather than `export *`: this was the only barrel in the
5
+ * package without one, so anything newly exported from `runtime.core.ts`
6
+ * silently became public API.
7
+ */
8
+ export { systemClock, systemClockSeconds, defineSecureRandom, systemRandom, FixedClock, SeededRandom, } from "./runtime.core.js";
2
9
  //# sourceMappingURL=index.js.map
@@ -13,21 +13,63 @@ export interface Clock {
13
13
  export interface ClockSeconds {
14
14
  nowSeconds(): number;
15
15
  }
16
- /** Returns cryptographically-secure random values. */
16
+ /**
17
+ * Brand marking an implementation as cryptographically secure.
18
+ *
19
+ * Structural typing alone would let any object with the right method names
20
+ * satisfy `Random`, including a seeded test generator. The brand makes the
21
+ * claim explicit: an implementor opts in through {@link defineSecureRandom}.
22
+ */
23
+ declare const SecureRandomBrand: unique symbol;
24
+ /**
25
+ * Returns cryptographically-secure random values.
26
+ *
27
+ * Implementations of this interface are safe for tokens, identifiers and
28
+ * secrets. A deterministic generator must implement {@link PseudoRandom}
29
+ * instead, so a test double cannot be injected where unpredictability is the
30
+ * requirement.
31
+ */
17
32
  export interface Random {
33
+ /** @internal Marks the implementation as unpredictable. */
34
+ readonly [SecureRandomBrand]: true;
18
35
  /** Returns a random UUID v4 string. */
19
36
  uuid(): string;
20
- /** Returns a random integer in [0, max). */
37
+ /** Returns a uniformly random integer in [0, max). */
21
38
  int(max: number): number;
22
39
  /** Returns a random string of the given length (alphanumeric). */
23
40
  string(length: number): string;
24
41
  /** Returns a random string of the given length from the given alphabet. */
25
42
  custom(length: number, alphabet: string): string;
26
43
  }
44
+ /**
45
+ * A reproducible generator for tests.
46
+ *
47
+ * Structurally identical to {@link Random} but nominally distinct, so it
48
+ * cannot be passed where a `Random` is required.
49
+ */
50
+ export interface PseudoRandom {
51
+ /** Marks this as a deterministic generator, not a secure one. */
52
+ readonly deterministic: true;
53
+ uuid(): string;
54
+ int(max: number): number;
55
+ string(length: number): string;
56
+ custom(length: number, alphabet: string): string;
57
+ }
27
58
  /** Default Clock implementation backed by `Date.now()`. */
28
59
  export declare const systemClock: Clock;
29
60
  /** Default ClockSeconds implementation backed by `Math.floor(Date.now() / 1000)`. */
30
61
  export declare const systemClockSeconds: ClockSeconds;
62
+ /**
63
+ * Declare an implementation cryptographically secure.
64
+ *
65
+ * Call this only for a generator whose output is genuinely unpredictable —
66
+ * one backed by `node:crypto`, Web Crypto, or a hardware source. It is the
67
+ * single supported way to produce a {@link Random}.
68
+ *
69
+ * @param implementation - The generator's operations.
70
+ * @returns The implementation, branded as secure.
71
+ */
72
+ export declare function defineSecureRandom(implementation: Omit<Random, typeof SecureRandomBrand>): Random;
31
73
  /** Default Random implementation backed by `node:crypto`. */
32
74
  export declare const systemRandom: Random;
33
75
  /** Deterministic Clock useful for tests. */
@@ -38,13 +80,24 @@ export declare class FixedClock implements Clock {
38
80
  set(time: number): void;
39
81
  advance(deltaMs: number): void;
40
82
  }
41
- /** Deterministic Random useful for tests. */
42
- export declare class SeededRandom implements Random {
83
+ /**
84
+ * Deterministic generator useful for tests.
85
+ *
86
+ * Implements {@link PseudoRandom}, never {@link Random}: its output is fully
87
+ * predictable from the seed, so injecting it where a secure generator is
88
+ * expected would make every token guessable from a single observation.
89
+ */
90
+ export declare class SeededRandom implements PseudoRandom {
91
+ readonly deterministic = true;
43
92
  private state;
44
93
  constructor(seed?: number);
94
+ /** Returns a structurally valid v4 UUID derived from the seed. */
45
95
  uuid(): string;
46
96
  int(max: number): number;
47
97
  string(length: number): string;
48
98
  custom(length: number, alphabet: string): string;
99
+ /** Advances the linear congruential state. */
100
+ private next;
49
101
  }
102
+ export {};
50
103
  //# sourceMappingURL=runtime.core.d.ts.map
@@ -5,6 +5,7 @@
5
5
  * avoid direct `Date.now()` and `Math.random()` calls. Tests can substitute
6
6
  * deterministic implementations.
7
7
  */
8
+ import { randomInt, randomUUID } from "node:crypto";
8
9
  /** Default Clock implementation backed by `Date.now()`. */
9
10
  export const systemClock = {
10
11
  now: () => Date.now(),
@@ -13,37 +14,41 @@ export const systemClock = {
13
14
  export const systemClockSeconds = {
14
15
  nowSeconds: () => Math.floor(Date.now() / 1000),
15
16
  };
16
- /** Default Random implementation backed by `node:crypto`. */
17
- export const systemRandom = {
18
- uuid: () => cryptoUUID(),
19
- int: (max) => cryptoInt(max),
20
- string: (length) => randomString(length, ALPHANUMERIC),
21
- custom: (length, alphabet) => randomString(length, alphabet),
22
- };
23
17
  const ALPHANUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
18
+ /** Returns a v4 UUID, preferring the Web Crypto implementation. */
24
19
  function cryptoUUID() {
25
20
  if (typeof globalThis.crypto?.randomUUID === "function") {
26
21
  return globalThis.crypto.randomUUID();
27
22
  }
28
- // node:crypto fallback (older Node, edge runtimes)
29
- // eslint-disable-next-line @typescript-eslint/no-require-imports
30
- const { randomUUID } = require("node:crypto");
31
23
  return randomUUID();
32
24
  }
25
+ /**
26
+ * Returns a uniformly random integer in [0, max).
27
+ *
28
+ * Rejection sampling, not `% max`. A modulo of a uniform 32-bit draw is only
29
+ * uniform when `max` is a power of two; for other bounds the low values come
30
+ * up more often, which is not acceptable from an interface documented as
31
+ * cryptographically secure.
32
+ */
33
33
  function cryptoInt(max) {
34
34
  if (!Number.isInteger(max) || max <= 0) {
35
35
  throw new RangeError("Random.int(max) requires a positive integer max");
36
36
  }
37
- if (typeof globalThis.crypto?.getRandomValues === "function") {
38
- const buf = new Uint32Array(1);
39
- globalThis.crypto.getRandomValues(buf);
40
- return buf[0] % max;
37
+ if (typeof globalThis.crypto?.getRandomValues !== "function") {
38
+ return randomInt(max);
39
+ }
40
+ const range = 2 ** 32;
41
+ const limit = range - (range % max);
42
+ const buffer = new Uint32Array(1);
43
+ for (;;) {
44
+ globalThis.crypto.getRandomValues(buffer);
45
+ const draw = buffer[0];
46
+ if (draw < limit)
47
+ return draw % max;
41
48
  }
42
- // eslint-disable-next-line @typescript-eslint/no-require-imports
43
- const { randomInt } = require("node:crypto");
44
- return randomInt(max);
45
49
  }
46
- function randomString(length, alphabet) {
50
+ /** Builds a random string over an alphabet. */
51
+ function randomString(length, alphabet, nextInt) {
47
52
  if (!Number.isInteger(length) || length < 0) {
48
53
  throw new RangeError("Random.string(length) requires a non-negative integer");
49
54
  }
@@ -52,10 +57,30 @@ function randomString(length, alphabet) {
52
57
  }
53
58
  let out = "";
54
59
  for (let i = 0; i < length; i++) {
55
- out += alphabet.charAt(cryptoInt(alphabet.length));
60
+ out += alphabet.charAt(nextInt(alphabet.length));
56
61
  }
57
62
  return out;
58
63
  }
64
+ /**
65
+ * Declare an implementation cryptographically secure.
66
+ *
67
+ * Call this only for a generator whose output is genuinely unpredictable —
68
+ * one backed by `node:crypto`, Web Crypto, or a hardware source. It is the
69
+ * single supported way to produce a {@link Random}.
70
+ *
71
+ * @param implementation - The generator's operations.
72
+ * @returns The implementation, branded as secure.
73
+ */
74
+ export function defineSecureRandom(implementation) {
75
+ return implementation;
76
+ }
77
+ /** Default Random implementation backed by `node:crypto`. */
78
+ export const systemRandom = defineSecureRandom({
79
+ uuid: () => cryptoUUID(),
80
+ int: (max) => cryptoInt(max),
81
+ string: (length) => randomString(length, ALPHANUMERIC, cryptoInt),
82
+ custom: (length, alphabet) => randomString(length, alphabet, cryptoInt),
83
+ });
59
84
  /** Deterministic Clock useful for tests. */
60
85
  export class FixedClock {
61
86
  current;
@@ -72,33 +97,46 @@ export class FixedClock {
72
97
  this.current += deltaMs;
73
98
  }
74
99
  }
75
- /** Deterministic Random useful for tests. */
100
+ /**
101
+ * Deterministic generator useful for tests.
102
+ *
103
+ * Implements {@link PseudoRandom}, never {@link Random}: its output is fully
104
+ * predictable from the seed, so injecting it where a secure generator is
105
+ * expected would make every token guessable from a single observation.
106
+ */
76
107
  export class SeededRandom {
108
+ deterministic = true;
77
109
  state;
78
110
  constructor(seed = 1) {
79
- this.state = seed;
111
+ this.state = seed >>> 0 || 1;
80
112
  }
113
+ /** Returns a structurally valid v4 UUID derived from the seed. */
81
114
  uuid() {
82
- const hex = this.state.toString(16).padStart(8, "0");
83
- this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
84
- return `${hex}-${this.state.toString(16).padStart(8, "0")}-0000-0000-000000000000`;
115
+ const hex = (count) => Array.from({ length: count }, () => this.next().toString(16).padStart(8, "0").slice(-1)).join("");
116
+ return [
117
+ hex(8),
118
+ hex(4),
119
+ `4${hex(3)}`,
120
+ `${"89ab".charAt(this.int(4))}${hex(3)}`,
121
+ hex(12),
122
+ ].join("-");
85
123
  }
86
124
  int(max) {
87
125
  if (!Number.isInteger(max) || max <= 0) {
88
- throw new RangeError("SeededRandom.int(max) requires a positive integer max");
126
+ throw new RangeError("PseudoRandom.int(max) requires a positive integer max");
89
127
  }
90
- this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
91
- return this.state % max;
128
+ return this.next() % max;
92
129
  }
93
130
  string(length) {
94
131
  return this.custom(length, ALPHANUMERIC);
95
132
  }
96
133
  custom(length, alphabet) {
97
- let out = "";
98
- for (let i = 0; i < length; i++) {
99
- out += alphabet.charAt(this.int(alphabet.length));
100
- }
101
- return out;
134
+ return randomString(length, alphabet, (max) => this.int(max));
135
+ }
136
+ /** Advances the linear congruential state. */
137
+ next() {
138
+ this.state = (Math.imul(this.state, 1103515245) + 12345) & 0x7fffffff;
139
+ return this.state;
102
140
  }
103
141
  }
104
142
  //# sourceMappingURL=runtime.core.js.map
@@ -5,18 +5,38 @@
5
5
  */
6
6
  /**
7
7
  * Safely parse JSON with a fallback value.
8
+ *
9
+ * "Safe" here means only that malformed JSON yields the fallback rather than
10
+ * throwing. The result is cast to `T` without validation — parse a trust
11
+ * boundary with `@zudojs/validation` or `@zudojs/schema` instead of relying on
12
+ * this cast.
13
+ *
14
+ * Keys that would reach a prototype are dropped, so the parsed value cannot
15
+ * seed a pollution chain downstream.
8
16
  */
9
17
  export declare function safeJsonParse<T>(json: string, fallback: T): T;
10
18
  /**
11
19
  * Convert a value to a string safely.
20
+ *
21
+ * Always returns a string. `JSON.stringify` returns the *value* `undefined`
22
+ * — not a string, and without throwing — for functions, symbols and
23
+ * `undefined`, so its result is checked rather than returned directly.
12
24
  */
13
25
  export declare function toString(value: unknown, fallback?: string): string;
14
26
  /**
15
- * Convert a value to a number safely.
27
+ * Convert a value to a finite number safely.
28
+ *
29
+ * Blank strings, hexadecimal literals and infinities all fall back rather than
30
+ * converting: a missing query parameter arriving as `""` becoming a real zero
31
+ * silently turns into a page size, a price or a limit.
16
32
  */
17
33
  export declare function toNumber(value: unknown, fallback?: number): number;
18
34
  /**
19
35
  * Convert a value to a boolean safely.
36
+ *
37
+ * `NaN` falls back rather than converting to true. It is what `toNumber`
38
+ * produces on failure, so chaining the two would otherwise turn a parse
39
+ * failure into the permissive answer for a flag.
20
40
  */
21
41
  export declare function toBoolean(value: unknown, fallback?: boolean): boolean;
22
42
  /**
@@ -25,6 +45,12 @@ export declare function toBoolean(value: unknown, fallback?: boolean): boolean;
25
45
  export declare function toArray<T>(value: T | T[]): T[];
26
46
  /**
27
47
  * Convert a Map to a plain object.
48
+ *
49
+ * Built on a null-prototype object with `defineProperty`. Assigning into an
50
+ * object literal routes a `__proto__` key through the prototype setter, so a
51
+ * Map built from request data — headers, form fields, query parameters — could
52
+ * replace the result's prototype with attacker-supplied values that
53
+ * `Object.keys` does not reveal.
28
54
  */
29
55
  export declare function mapToObject<K extends string | number | symbol, V>(map: Map<K, V>): Record<K, V>;
30
56
  /**
@@ -36,7 +62,11 @@ export declare function objectToMap<K extends string | number | symbol, V>(obj:
36
62
  */
37
63
  export declare function snakeToCamel(str: string): string;
38
64
  /**
39
- * Convert camelCase to snake_case.
65
+ * Convert camelCase or PascalCase to snake_case.
66
+ *
67
+ * Leading capitals do not produce a leading separator, and acronyms survive
68
+ * as single words — an identifier like `_hello_world` is not a valid column
69
+ * name, and `parse_h_t_t_p_response` is not a useful one.
40
70
  */
41
71
  export declare function camelToSnake(str: string): string;
42
72
  /**
@@ -44,7 +74,7 @@ export declare function camelToSnake(str: string): string;
44
74
  */
45
75
  export declare function kebabToCamel(str: string): string;
46
76
  /**
47
- * Convert camelCase to kebab-case.
77
+ * Convert camelCase or PascalCase to kebab-case.
48
78
  */
49
79
  export declare function camelToKebab(str: string): string;
50
80
  //# sourceMappingURL=typeConverters.core.d.ts.map
@@ -3,12 +3,30 @@
3
3
  *
4
4
  * @module typeConverters/typeConverters
5
5
  */
6
+ /** Property names that mutate a prototype instead of adding a key. */
7
+ const UNSAFE_KEYS = new Set([
8
+ "__proto__",
9
+ "constructor",
10
+ "prototype",
11
+ ]);
6
12
  /**
7
13
  * Safely parse JSON with a fallback value.
14
+ *
15
+ * "Safe" here means only that malformed JSON yields the fallback rather than
16
+ * throwing. The result is cast to `T` without validation — parse a trust
17
+ * boundary with `@zudojs/validation` or `@zudojs/schema` instead of relying on
18
+ * this cast.
19
+ *
20
+ * Keys that would reach a prototype are dropped, so the parsed value cannot
21
+ * seed a pollution chain downstream.
8
22
  */
9
23
  export function safeJsonParse(json, fallback) {
10
24
  try {
11
- return JSON.parse(json);
25
+ return JSON.parse(json, function reviver(key, value) {
26
+ if (UNSAFE_KEYS.has(key))
27
+ return undefined;
28
+ return value;
29
+ });
12
30
  }
13
31
  catch {
14
32
  return fallback;
@@ -16,48 +34,81 @@ export function safeJsonParse(json, fallback) {
16
34
  }
17
35
  /**
18
36
  * Convert a value to a string safely.
37
+ *
38
+ * Always returns a string. `JSON.stringify` returns the *value* `undefined`
39
+ * — not a string, and without throwing — for functions, symbols and
40
+ * `undefined`, so its result is checked rather than returned directly.
19
41
  */
20
42
  export function toString(value, fallback = "") {
21
43
  if (value === null || value === undefined)
22
44
  return fallback;
23
45
  if (typeof value === "string")
24
46
  return value;
25
- if (typeof value === "number" || typeof value === "boolean")
47
+ if (typeof value === "number" || typeof value === "boolean") {
26
48
  return String(value);
49
+ }
50
+ if (typeof value === "bigint")
51
+ return `${value}`;
52
+ if (typeof value === "symbol")
53
+ return value.toString();
54
+ if (typeof value === "function")
55
+ return fallback;
27
56
  try {
28
- return JSON.stringify(value);
57
+ const serialized = JSON.stringify(value);
58
+ return typeof serialized === "string" ? serialized : fallback;
29
59
  }
30
60
  catch {
31
61
  return fallback;
32
62
  }
33
63
  }
34
64
  /**
35
- * Convert a value to a number safely.
65
+ * Convert a value to a finite number safely.
66
+ *
67
+ * Blank strings, hexadecimal literals and infinities all fall back rather than
68
+ * converting: a missing query parameter arriving as `""` becoming a real zero
69
+ * silently turns into a page size, a price or a limit.
36
70
  */
37
71
  export function toNumber(value, fallback = NaN) {
38
- if (typeof value === "number")
39
- return value;
72
+ if (typeof value === "number") {
73
+ return Number.isFinite(value) ? value : fallback;
74
+ }
40
75
  if (typeof value === "string") {
41
- const num = Number(value);
42
- return Number.isNaN(num) ? fallback : num;
76
+ const trimmed = value.trim();
77
+ if (trimmed.length === 0)
78
+ return fallback;
79
+ if (!/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/u.test(trimmed)) {
80
+ return fallback;
81
+ }
82
+ const parsed = Number(trimmed);
83
+ return Number.isFinite(parsed) ? parsed : fallback;
43
84
  }
44
85
  return fallback;
45
86
  }
46
87
  /**
47
88
  * Convert a value to a boolean safely.
89
+ *
90
+ * `NaN` falls back rather than converting to true. It is what `toNumber`
91
+ * produces on failure, so chaining the two would otherwise turn a parse
92
+ * failure into the permissive answer for a flag.
48
93
  */
49
94
  export function toBoolean(value, fallback = false) {
50
95
  if (typeof value === "boolean")
51
96
  return value;
52
97
  if (typeof value === "string") {
53
98
  const lower = value.toLowerCase().trim();
54
- if (lower === "true" || lower === "1" || lower === "yes")
99
+ if (lower === "true" || lower === "1" || lower === "yes" || lower === "on")
55
100
  return true;
56
- if (lower === "false" || lower === "0" || lower === "no" || lower === "")
101
+ if (lower === "false" ||
102
+ lower === "0" ||
103
+ lower === "no" ||
104
+ lower === "off" ||
105
+ lower === "")
57
106
  return false;
107
+ return fallback;
108
+ }
109
+ if (typeof value === "number") {
110
+ return Number.isNaN(value) ? fallback : value !== 0;
58
111
  }
59
- if (typeof value === "number")
60
- return value !== 0;
61
112
  return fallback;
62
113
  }
63
114
  /**
@@ -70,11 +121,22 @@ export function toArray(value) {
70
121
  }
71
122
  /**
72
123
  * Convert a Map to a plain object.
124
+ *
125
+ * Built on a null-prototype object with `defineProperty`. Assigning into an
126
+ * object literal routes a `__proto__` key through the prototype setter, so a
127
+ * Map built from request data — headers, form fields, query parameters — could
128
+ * replace the result's prototype with attacker-supplied values that
129
+ * `Object.keys` does not reveal.
73
130
  */
74
131
  export function mapToObject(map) {
75
- const obj = {};
132
+ const obj = Object.create(null);
76
133
  for (const [key, value] of map) {
77
- obj[key] = value;
134
+ Object.defineProperty(obj, key, {
135
+ value,
136
+ enumerable: true,
137
+ writable: true,
138
+ configurable: true,
139
+ });
78
140
  }
79
141
  return obj;
80
142
  }
@@ -88,24 +150,41 @@ export function objectToMap(obj) {
88
150
  * Convert snake_case to camelCase.
89
151
  */
90
152
  export function snakeToCamel(str) {
91
- return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
153
+ return str.replace(/(?<!_)_+([a-z0-9])/gu, (_, char) => char.toUpperCase());
154
+ }
155
+ /**
156
+ * Split a camelCase or PascalCase identifier into its words.
157
+ *
158
+ * Runs of capitals are kept together, so `parseHTTPResponse` yields
159
+ * `["parse", "HTTP", "Response"]` rather than one word per letter.
160
+ */
161
+ function splitCamelWords(str) {
162
+ return str.match(/(?:[A-Z](?![a-z]))+|[A-Z]?[a-z0-9]+|[A-Z]|[0-9]+/gu) ?? [];
92
163
  }
93
164
  /**
94
- * Convert camelCase to snake_case.
165
+ * Convert camelCase or PascalCase to snake_case.
166
+ *
167
+ * Leading capitals do not produce a leading separator, and acronyms survive
168
+ * as single words — an identifier like `_hello_world` is not a valid column
169
+ * name, and `parse_h_t_t_p_response` is not a useful one.
95
170
  */
96
171
  export function camelToSnake(str) {
97
- return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
172
+ return splitCamelWords(str)
173
+ .map((word) => word.toLowerCase())
174
+ .join("_");
98
175
  }
99
176
  /**
100
177
  * Convert kebab-case to camelCase.
101
178
  */
102
179
  export function kebabToCamel(str) {
103
- return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
180
+ return str.replace(/(?<!-)-+([a-z0-9])/gu, (_, char) => char.toUpperCase());
104
181
  }
105
182
  /**
106
- * Convert camelCase to kebab-case.
183
+ * Convert camelCase or PascalCase to kebab-case.
107
184
  */
108
185
  export function camelToKebab(str) {
109
- return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
186
+ return splitCamelWords(str)
187
+ .map((word) => word.toLowerCase())
188
+ .join("-");
110
189
  }
111
190
  //# sourceMappingURL=typeConverters.core.js.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module typeGuards
5
5
  */
6
- export { isPlainObject, isNonNullObject, isNonEmptyString, isPositiveNumber, isInteger, isDate, isUrl, isEmail, isUuid, isIsoDateString, isArrayOfType, isDefined, isFunction, isPromise, } from "./typeGuards.core.js";
6
+ export { MAX_EMAIL_LENGTH, isPlainObject, isNonNullObject, isNonEmptyString, isPositiveNumber, isFiniteNumber, isInteger, isDate, isUrl, isEmail, isUuid, isUuidV4, isIsoDateString, isIsoDateTimeString, isArrayOfType, isDefined, isFunction, isPromise, isThenable, } from "./typeGuards.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module typeGuards
5
5
  */
6
- export { isPlainObject, isNonNullObject, isNonEmptyString, isPositiveNumber, isInteger, isDate, isUrl, isEmail, isUuid, isIsoDateString, isArrayOfType, isDefined, isFunction, isPromise, } from "./typeGuards.core.js";
6
+ export { MAX_EMAIL_LENGTH, isPlainObject, isNonNullObject, isNonEmptyString, isPositiveNumber, isFiniteNumber, isInteger, isDate, isUrl, isEmail, isUuid, isUuidV4, isIsoDateString, isIsoDateTimeString, isArrayOfType, isDefined, isFunction, isPromise, isThenable, } from "./typeGuards.core.js";
7
7
  //# sourceMappingURL=index.js.map