-
Notifications
You must be signed in to change notification settings - Fork 0
/
mod.ts
146 lines (133 loc) · 3.57 KB
/
mod.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/**
* Symbol used for private access to the inner value
*/
const secretValue = Symbol("secretValue");
/**
* A secret value that cannot be accessed directly.
*
* @example
* const secret = secret('password');
* console.log(secret); // '[REDACTED]'
* console.log(secret.expose()); // 'password'
*/
export class Secret<T> {
private [secretValue]: T;
private static redactedString = "[REDACTED]";
constructor(value: T) {
this[secretValue] = value;
// Prevent the secret from appearing in toString
Object.defineProperty(this, "toString", {
value: () => Secret.redactedString,
enumerable: false,
writable: false,
configurable: false,
});
// Prevent JSON serialization
Object.defineProperty(this, "toJSON", {
value: () => Secret.redactedString,
enumerable: false,
writable: false,
configurable: false,
});
// Freeze the object to prevent modifications
Object.freeze(this);
}
/**
* Exposes the secret value.
*
* @returns The secret value.
*
* @example
* const secret = secret('password');
* console.log(secret.expose()); // 'password'
*/
expose(): T {
return this[secretValue];
}
/**
* Maps the secret value to a new secret value.
*
* @param fn - The function to map the secret value.
* @returns A new secret with the mapped value.
*
* @example
* const secret = secret('password');
* const mapped = secret.map(value => value.length);
* console.log(mapped.expose()); // 8
*/
map<U>(fn: (value: T) => U): Secret<U> {
return new Secret(fn(this[secretValue]));
}
/**
* Compares the secret value to another secret value.
*
* @param other - The other secret to compare to.
* @returns Whether the secret values are equal.
*
* @example
* const secret1 = secret('password');
* const secret2 = secret('password');
* console.log(secret1.equals(secret2)); // true
*
* const secret2 = secret('password1');
* console.log(secret1.equals(secret2)); // false
*/
equals(other: unknown): boolean {
return other instanceof Secret && this[secretValue] === other[secretValue];
}
/**
* Clones the secret value.
*
* @returns A new secret with the same value.
*
* @example
* const secret = secret('password');
* const cloned = secret.clone();
* console.log(cloned.expose()); // 'password'
* console.log(Object.is(secret, cloned)); // false
* console.log(secret.equals(cloned)); // true
*/
clone(): Secret<T> {
return new Secret(this[secretValue]);
}
[Symbol.for("nodejs.util.inspect.custom")](): string {
return Secret.redactedString;
}
}
/**
* Type guard to check if a value is a Secret.
*
* @param value - The value to check.
* @returns Whether the value is a Secret.
*
* @example
* const secret = secret('password');
* console.log(isSecret(secret)); // true
* console.log(isSecret('password')); // false
*/
export function isSecret<T>(value: unknown): value is Secret<T> {
return value instanceof Secret;
}
/**
* Asserts that a value is a Secret.
*
* @param value - The value to assert.
* @throws {SecretError} If the value is not a Secret.
*
* @example
* const secret = assertSecret('password');
* console.log(secret.expose()); // 'password'
*/
export function assertSecret<T>(value: unknown): asserts value is Secret<T> {
if (!isSecret(value)) {
throw new SecretError("Expected a Secret");
}
}
/**
* The error thrown by assertSecret, if the value is not a Secret.
*/
export class SecretError extends Error {
constructor(message: string) {
super(message);
}
}