@zudojs/types 0.0.1 → 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.
- package/README.md +44 -6
- package/dist/runtime/index.d.ts +9 -1
- package/dist/runtime/index.js +8 -1
- package/dist/runtime/runtime.core.d.ts +57 -4
- package/dist/runtime/runtime.core.js +70 -32
- package/dist/typeConverters/typeConverters.core.d.ts +33 -3
- package/dist/typeConverters/typeConverters.core.js +99 -20
- package/dist/typeGuards/index.d.ts +1 -1
- package/dist/typeGuards/index.js +1 -1
- package/dist/typeGuards/typeGuards.core.d.ts +51 -4
- package/dist/typeGuards/typeGuards.core.js +110 -13
- package/dist/typeUtilities/index.d.ts +1 -1
- package/dist/typeUtilities/typeUtilities.core.d.ts +22 -3
- package/package.json +12 -5
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/runtime.core.d.ts.map +0 -1
- package/dist/runtime/runtime.core.js.map +0 -1
- package/dist/typeConverters/index.d.ts.map +0 -1
- package/dist/typeConverters/index.js.map +0 -1
- package/dist/typeConverters/typeConverters.core.d.ts.map +0 -1
- package/dist/typeConverters/typeConverters.core.js.map +0 -1
- package/dist/typeGuards/index.d.ts.map +0 -1
- package/dist/typeGuards/index.js.map +0 -1
- package/dist/typeGuards/typeGuards.core.d.ts.map +0 -1
- package/dist/typeGuards/typeGuards.core.js.map +0 -1
- package/dist/typeUtilities/index.d.ts.map +0 -1
- package/dist/typeUtilities/index.js.map +0 -1
- package/dist/typeUtilities/typeUtilities.core.d.ts.map +0 -1
- package/dist/typeUtilities/typeUtilities.core.js.map +0 -1
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
|
|
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
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
42
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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.
|
|
83
|
-
|
|
84
|
-
|
|
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("
|
|
126
|
+
throw new RangeError("PseudoRandom.int(max) requires a positive integer max");
|
|
89
127
|
}
|
|
90
|
-
this.
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
|
|
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" ||
|
|
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
|
|
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-
|
|
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
|
|
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(
|
|
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
|
|
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
|
package/dist/typeGuards/index.js
CHANGED
|
@@ -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
|