@scure/btc-signer 2.0.0 → 2.2.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/src/utils.ts CHANGED
@@ -1,34 +1,168 @@
1
1
  import { schnorr, secp256k1 as secp } from '@noble/curves/secp256k1.js';
2
- import { bytesToNumberBE, numberToBytesBE } from '@noble/curves/utils.js';
2
+ import { abytes, bytesToNumberBE, numberToBytesBE } from '@noble/curves/utils.js';
3
3
  import { ripemd160 } from '@noble/hashes/legacy.js';
4
- import { sha256 } from '@noble/hashes/sha2.js';
4
+ import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js';
5
+ import { type TArg, type TRet } from '@noble/hashes/utils.js';
5
6
  import { utils as packedUtils, U32LE } from 'micro-packed';
7
+ export { type TArg, type TRet } from '@noble/hashes/utils.js';
6
8
 
9
+ /** Hex-like input accepted by helpers in this module. */
7
10
  export type Hex = string | Uint8Array;
11
+ /** Byte array alias used across the library. */
8
12
  export type Bytes = Uint8Array;
9
- const Point = secp.Point;
10
- const Fn = Point.Fn;
11
- const CURVE_ORDER = Point.Fn.ORDER;
13
+
14
+ const Point = /* @__PURE__ */ (() => secp.Point)();
15
+ const Fn = /* @__PURE__ */ (() => Point.Fn)();
16
+ const CURVE_ORDER = /* @__PURE__ */ (() => Point.Fn.ORDER)();
17
+ /**
18
+ * Checks whether a curve y-coordinate is even.
19
+ * @param y - y-coordinate to inspect
20
+ * @returns `true` when the coordinate is even.
21
+ * @example
22
+ * Check whether a point coordinate has even parity.
23
+ * ```ts
24
+ * hasEven(2n);
25
+ * ```
26
+ */
12
27
  export const hasEven = (y: bigint) => y % 2n === 0n;
13
28
 
14
- const isBytes: (a: unknown) => a is Uint8Array = packedUtils.isBytes;
15
- const concatBytes: (...arrays: Uint8Array[]) => Uint8Array = packedUtils.concatBytes;
16
- const equalBytes: (a: Uint8Array, b: Uint8Array) => boolean = packedUtils.equalBytes;
17
- export { concatBytes, equalBytes, isBytes, sha256 };
29
+ /**
30
+ * Checks whether a value is a Uint8Array.
31
+ * @param a - value to inspect
32
+ * @returns `true` when the value is a Uint8Array.
33
+ * @example
34
+ * Check whether an unknown value is already bytes.
35
+ * ```ts
36
+ * isBytes(new Uint8Array([1]));
37
+ * ```
38
+ */
39
+ export const isBytes: (a: unknown) => a is Uint8Array = /* @__PURE__ */ (() =>
40
+ packedUtils.isBytes)();
41
+ /**
42
+ * Concatenates byte arrays into a single Uint8Array.
43
+ * @param arrays - byte arrays to concatenate
44
+ * @returns Concatenated byte array.
45
+ * @example
46
+ * Join several byte chunks before hashing or signing them.
47
+ * ```ts
48
+ * concatBytes(new Uint8Array([1]), new Uint8Array([2]));
49
+ * ```
50
+ */
51
+ export const concatBytes: (...arrays: TArg<Uint8Array[]>) => TRet<Uint8Array> =
52
+ /* @__PURE__ */ (() =>
53
+ packedUtils.concatBytes as (...arrays: TArg<Uint8Array[]>) => TRet<Uint8Array>)();
54
+ /**
55
+ * Compares two byte arrays for equality.
56
+ * @param a - first byte array
57
+ * @param b - second byte array
58
+ * @returns `true` when both arrays contain the same bytes.
59
+ * @example
60
+ * Compare two serialized values without converting them first.
61
+ * ```ts
62
+ * equalBytes(new Uint8Array([1]), new Uint8Array([1]));
63
+ * ```
64
+ */
65
+ export const equalBytes: (a: TArg<Uint8Array>, b: TArg<Uint8Array>) => boolean =
66
+ /* @__PURE__ */ (() =>
67
+ packedUtils.equalBytes as (a: TArg<Uint8Array>, b: TArg<Uint8Array>) => boolean)();
68
+ /**
69
+ * SHA-256 hash function.
70
+ * @param msg - bytes to hash
71
+ * @returns SHA-256 digest.
72
+ * @example
73
+ * Hash a byte array with SHA-256.
74
+ * ```ts
75
+ * sha256(new Uint8Array([1, 2, 3]));
76
+ * ```
77
+ */
78
+ export const sha256: typeof nobleSha256 = /* @__PURE__ */ (() => nobleSha256)();
18
79
 
19
- export const hash160 = (msg: Uint8Array): Uint8Array => ripemd160(sha256(msg));
20
- export const sha256x2 = (...msgs: Uint8Array[]): Uint8Array => sha256(sha256(concatBytes(...msgs)));
21
- export const randomPrivateKeyBytes: () => Uint8Array = schnorr.utils.randomSecretKey;
22
- export const pubSchnorr = schnorr.getPublicKey as (priv: string | Uint8Array) => Uint8Array;
23
- export const pubECDSA: (privateKey: Uint8Array, isCompressed?: boolean) => Uint8Array =
24
- secp.getPublicKey;
80
+ /**
81
+ * HASH160 helper used by classic Bitcoin addresses.
82
+ * @param msg - bytes to hash
83
+ * @returns RIPEMD160(SHA256(msg)).
84
+ * @example
85
+ * Derive the HASH160 used by legacy address formats.
86
+ * ```ts
87
+ * hash160(new Uint8Array([1, 2, 3]));
88
+ * ```
89
+ */
90
+ export const hash160 = (msg: TArg<Uint8Array>): TRet<Uint8Array> =>
91
+ ripemd160(sha256(msg)) as TRet<Uint8Array>;
92
+ /**
93
+ * Double-SHA256 helper used by Bitcoin transaction ids.
94
+ * @param msgs - message parts to concatenate and hash
95
+ * @returns SHA256(SHA256(concat(msgs))).
96
+ * @example
97
+ * Compute the double-SHA256 used by txids and sighashes.
98
+ * ```ts
99
+ * sha256x2(new Uint8Array([1]), new Uint8Array([2]));
100
+ * ```
101
+ */
102
+ export const sha256x2 = (...msgs: TArg<Uint8Array[]>): TRet<Uint8Array> =>
103
+ sha256(sha256(concatBytes(...msgs))) as TRet<Uint8Array>;
104
+ /**
105
+ * Generates a random secp256k1 private key.
106
+ * @returns Random 32-byte private key.
107
+ * @example
108
+ * Generate a fresh secp256k1 private key for signing.
109
+ * ```ts
110
+ * const privKey = randomPrivateKeyBytes();
111
+ * ```
112
+ */
113
+ export const randomPrivateKeyBytes = (): TRet<Uint8Array> =>
114
+ schnorr.utils.randomSecretKey() as TRet<Uint8Array>;
115
+ /**
116
+ * Derives a BIP340 Schnorr public key from a private key.
117
+ * @param priv - private key bytes
118
+ * @returns X-only public key bytes.
119
+ * @example
120
+ * Derive the x-only public key used by Schnorr and Taproot.
121
+ * ```ts
122
+ * import { pubSchnorr, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
123
+ * pubSchnorr(randomPrivateKeyBytes());
124
+ * ```
125
+ */
126
+ export const pubSchnorr = (priv: TArg<Uint8Array>): TRet<Uint8Array> =>
127
+ schnorr.getPublicKey(priv) as TRet<Uint8Array>;
128
+ /**
129
+ * Derives a secp256k1 ECDSA public key from a private key.
130
+ * @param privateKey - private key bytes
131
+ * @param isCompressed - whether to return the compressed form
132
+ * @returns Serialized public key bytes.
133
+ * @example
134
+ * Derive the normal secp256k1 public key for legacy or SegWit scripts.
135
+ * ```ts
136
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
137
+ * pubECDSA(randomPrivateKeyBytes());
138
+ * ```
139
+ */
140
+ export const pubECDSA = (privateKey: TArg<Uint8Array>, isCompressed?: boolean): TRet<Uint8Array> =>
141
+ secp.getPublicKey(privateKey, isCompressed) as TRet<Uint8Array>;
25
142
 
26
143
  // low-r signature grinding. Used to reduce tx size by 1 byte.
27
144
  // noble/secp256k1 does not support the feature: it is not used outside of BTC.
28
145
  // We implement it manually, because in BTC it's common.
29
146
  // Not best way, but closest to bitcoin implementation (easier to check)
30
147
  const hasLowR = (sig: { r: bigint; s: bigint }) => sig.r < CURVE_ORDER / 2n;
31
- export function signECDSA(hash: Bytes, privateKey: Bytes, lowR = false): Bytes {
148
+ /**
149
+ * Signs a 32-byte hash with ECDSA and returns DER encoding.
150
+ * @param hash - message hash to sign
151
+ * @param privateKey - signer private key
152
+ * @param lowR - whether to grind for low-R signatures
153
+ * @returns DER-encoded signature bytes.
154
+ * @throws If low-R grinding overflows or ECDSA signing fails validation. {@link Error}
155
+ * @example
156
+ * Hash a message first, then create the DER-encoded ECDSA signature.
157
+ * ```ts
158
+ * import { randomPrivateKeyBytes, sha256, signECDSA } from '@scure/btc-signer/utils.js';
159
+ * signECDSA(sha256(new Uint8Array([1, 2, 3])), randomPrivateKeyBytes());
160
+ * ```
161
+ */
162
+ export function signECDSA(hash: TArg<Bytes>, privateKey: TArg<Bytes>, lowR = false): TRet<Bytes> {
163
+ // signECDSA is the 32-byte sighash wrapper for BTC callers, so reject arbitrary-length
164
+ // messages here instead of silently signing them with prehash disabled.
165
+ abytes(hash, 32, 'hash');
32
166
  let sig = secp.Signature.fromBytes(secp.sign(hash, privateKey, { prehash: false }));
33
167
  if (lowR && !hasLowR(sig)) {
34
168
  const extraEntropy = new Uint8Array(32);
@@ -39,34 +173,100 @@ export function signECDSA(hash: Bytes, privateKey: Bytes, lowR = false): Bytes {
39
173
  if (counter > 4294967295) throw new Error('lowR counter overflow: report the error');
40
174
  }
41
175
  }
42
- return sig.toBytes('der');
176
+ return sig.toBytes('der') as TRet<Bytes>;
43
177
  }
44
178
 
45
- export const signSchnorr: typeof schnorr.sign = schnorr.sign;
46
- export const tagSchnorr: typeof schnorr.utils.taggedHash = schnorr.utils.taggedHash;
179
+ /**
180
+ * BIP340 Schnorr signing function.
181
+ * @param message - 32-byte message digest
182
+ * @param secretKey - signer private key
183
+ * @param auxRand - optional auxiliary randomness
184
+ * @returns Schnorr signature bytes.
185
+ * @example
186
+ * Sign a 32-byte digest with the built-in BIP340 helper.
187
+ * ```ts
188
+ * import { randomPrivateKeyBytes, sha256, signSchnorr } from '@scure/btc-signer/utils.js';
189
+ * const msg = sha256(new Uint8Array([1, 2, 3]));
190
+ * signSchnorr(msg, randomPrivateKeyBytes());
191
+ * ```
192
+ */
193
+ export const signSchnorr = (
194
+ message: TArg<Uint8Array>,
195
+ secretKey: TArg<Uint8Array>,
196
+ auxRand?: TArg<Uint8Array>
197
+ ): TRet<Uint8Array> => schnorr.sign(message, secretKey, auxRand) as TRet<Uint8Array>;
198
+ /**
199
+ * Tagged-hash helper used by Schnorr and taproot constructions.
200
+ * @param tag - tagged-hash domain separator
201
+ * @param messages - message parts hashed under the tag
202
+ * @returns Tagged SHA-256 digest.
203
+ * @example
204
+ * Build the tagged hash used by Taproot leaves or tweaks.
205
+ * ```ts
206
+ * import { tagSchnorr } from '@scure/btc-signer/utils.js';
207
+ * tagSchnorr('TapLeaf', Uint8Array.of(0xc0), Uint8Array.of(0x51));
208
+ * ```
209
+ */
210
+ export const tagSchnorr = (tag: string, ...messages: TArg<Uint8Array[]>): TRet<Uint8Array> =>
211
+ schnorr.utils.taggedHash(tag, ...messages) as TRet<Uint8Array>;
47
212
 
48
- export const PubT = {
49
- ecdsa: 0,
50
- schnorr: 1,
51
- };
213
+ /** Public key format tags used by validation helpers. */
214
+ export const PubT = /* @__PURE__ */ (() =>
215
+ Object.freeze({
216
+ ecdsa: 0,
217
+ schnorr: 1,
218
+ }))();
219
+ /** Numeric public key format tag from {@link PubT}. */
52
220
  export type PubT = ValueOf<typeof PubT>;
53
221
 
54
- export function validatePubkey(pub: Bytes, type: PubT): Bytes {
222
+ /**
223
+ * Validates a public key against the expected Bitcoin key encoding.
224
+ * @param pub - public key bytes to validate
225
+ * @param type - expected public key format
226
+ * @returns The validated public key bytes.
227
+ * @throws On wrong argument types. {@link TypeError}
228
+ * @throws On wrong argument ranges or values. {@link RangeError}
229
+ * @example
230
+ * Reject keys that do not match the encoding required by the current script path.
231
+ * ```ts
232
+ * import {
233
+ * PubT,
234
+ * pubECDSA,
235
+ * randomPrivateKeyBytes,
236
+ * validatePubkey,
237
+ * } from '@scure/btc-signer/utils.js';
238
+ * validatePubkey(pubECDSA(randomPrivateKeyBytes()), PubT.ecdsa);
239
+ * ```
240
+ */
241
+ export function validatePubkey(pub: TArg<Bytes>, type: PubT): TRet<Bytes> {
55
242
  const len = pub.length;
56
243
  if (type === PubT.ecdsa) {
57
- if (len === 32) throw new Error('Expected non-Schnorr key');
244
+ if (len === 32) throw new RangeError('Expected non-Schnorr key');
58
245
  Point.fromBytes(pub); // does assertValidity
59
- return pub;
246
+ return pub as TRet<Bytes>;
60
247
  } else if (type === PubT.schnorr) {
61
- if (len !== 32) throw new Error('Expected 32-byte Schnorr key');
248
+ if (len !== 32) throw new RangeError('Expected 32-byte Schnorr key');
62
249
  schnorr.utils.lift_x(bytesToNumberBE(pub));
63
- return pub;
250
+ return pub as TRet<Bytes>;
64
251
  } else {
65
- throw new Error('Unknown key type');
252
+ throw new TypeError('Unknown key type');
66
253
  }
67
254
  }
68
255
 
69
- export function tapTweak(a: Bytes, b: Bytes): bigint {
256
+ /**
257
+ * Computes the Taproot tweak scalar from an internal key and merkle root.
258
+ * @param a - internal key bytes
259
+ * @param b - optional merkle root bytes
260
+ * @returns Taproot tweak scalar.
261
+ * @throws If the tweak scalar is outside the curve order. {@link Error}
262
+ * @example
263
+ * Combine the internal key and Merkle root into the Taproot tweak scalar.
264
+ * ```ts
265
+ * import { pubSchnorr, randomPrivateKeyBytes, tapTweak } from '@scure/btc-signer/utils.js';
266
+ * tapTweak(pubSchnorr(randomPrivateKeyBytes()), new Uint8Array());
267
+ * ```
268
+ */
269
+ export function tapTweak(a: TArg<Bytes>, b: TArg<Bytes>): bigint {
70
270
  const u = schnorr.utils;
71
271
  const t = u.taggedHash('TapTweak', a, b);
72
272
  const tn = bytesToNumberBE(t);
@@ -74,8 +274,27 @@ export function tapTweak(a: Bytes, b: Bytes): bigint {
74
274
  return tn;
75
275
  }
76
276
 
77
- export function taprootTweakPrivKey(privKey: Bytes, merkleRoot: Bytes = Uint8Array.of()): Bytes {
277
+ /**
278
+ * Tweaks a private key for Taproot key-path spending.
279
+ * @param privKey - internal private key bytes
280
+ * @param merkleRoot - optional taproot merkle root
281
+ * @returns Tweaked private key bytes.
282
+ * @throws If the Taproot tweak scalar is outside the curve order. {@link Error}
283
+ * @example
284
+ * Derive the tweaked Taproot key-path secret from the internal private key.
285
+ * ```ts
286
+ * import { randomPrivateKeyBytes, taprootTweakPrivKey } from '@scure/btc-signer/utils.js';
287
+ * taprootTweakPrivKey(randomPrivateKeyBytes());
288
+ * ```
289
+ */
290
+ export function taprootTweakPrivKey(
291
+ privKey: TArg<Bytes>,
292
+ merkleRoot: TArg<Bytes> = Uint8Array.of()
293
+ ): TRet<Bytes> {
78
294
  const u = schnorr.utils;
295
+ // BIP341 taproot_tweak_seckey starts with `seckey0 = int_from_bytes(seckey0)`, and
296
+ // BIP340 defines `int(x)` only for `x` as a 32-byte array, so reject other widths here.
297
+ abytes(privKey, 32, 'privKey');
79
298
  const seckey0 = bytesToNumberBE(privKey); // seckey0 = int_from_bytes(seckey0)
80
299
  const P = Point.BASE.multiply(seckey0); // P = point_mul(G, seckey0)
81
300
  // seckey = seckey0 if has_even_y(P) else SECP256K1_ORDER - seckey0
@@ -84,49 +303,95 @@ export function taprootTweakPrivKey(privKey: Bytes, merkleRoot: Bytes = Uint8Arr
84
303
  // t = int_from_bytes(tagged_hash("TapTweak", bytes_from_int(x(P)) + h)); >= SECP256K1_ORDER check
85
304
  const t = tapTweak(xP, merkleRoot);
86
305
  // bytes_from_int((seckey + t) % SECP256K1_ORDER)
87
- return numberToBytesBE(Fn.add(seckey, t), 32);
306
+ return numberToBytesBE(Fn.add(seckey, t), 32) as TRet<Bytes>;
88
307
  }
89
308
 
90
- export function taprootTweakPubkey(pubKey: Bytes, h: Bytes): [Bytes, number] {
309
+ /**
310
+ * Tweaks a Schnorr public key for Taproot key-path spending.
311
+ * @param pubKey - x-only internal public key
312
+ * @param h - taproot merkle root
313
+ * @returns Tweaked public key and output-key parity.
314
+ * @throws If the Taproot tweak scalar is outside the curve order. {@link Error}
315
+ * @example
316
+ * Derive the final Taproot output key from the internal key and Merkle root.
317
+ * ```ts
318
+ * import {
319
+ * pubSchnorr,
320
+ * randomPrivateKeyBytes,
321
+ * taprootTweakPubkey,
322
+ * } from '@scure/btc-signer/utils.js';
323
+ * taprootTweakPubkey(pubSchnorr(randomPrivateKeyBytes()), new Uint8Array());
324
+ * ```
325
+ */
326
+ export function taprootTweakPubkey(pubKey: TArg<Bytes>, h: TArg<Bytes>): TRet<[Bytes, number]> {
91
327
  const u = schnorr.utils;
328
+ // BIP341 taproot_tweak_pubkey feeds `pubkey` into `int_from_bytes(pubkey)`, and
329
+ // BIP340 defines `int(x)` only for `x` as a 32-byte array, so reject other widths here.
330
+ abytes(pubKey, 32, 'pubKey');
92
331
  const t = tapTweak(pubKey, h); // t = int_from_bytes(tagged_hash("TapTweak", pubkey + h))
93
332
  const P = u.lift_x(bytesToNumberBE(pubKey)); // P = lift_x(int_from_bytes(pubkey))
94
333
  const Q = P.add(Point.BASE.multiply(t)); // Q = point_add(P, point_mul(G, t))
95
334
  const parity = hasEven(Q.y) ? 0 : 1; // 0 if has_even_y(Q) else 1
96
- return [u.pointToBytes(Q), parity]; // bytes_from_int(x(Q))
335
+ return [u.pointToBytes(Q), parity] as TRet<[Bytes, number]>; // bytes_from_int(x(Q))
97
336
  }
98
337
 
99
338
  // Another stupid decision, where lack of standard affects security.
100
339
  // Multisig needs to be generated with some key.
101
- // We are using approach from BIP 341/bitcoinjs-lib: SHA256(uncompressedDER(SECP256K1_GENERATOR_POINT))
340
+ // We are using the BIP 341/bitcoinjs-lib approach:
341
+ // SHA256(uncompressedDER(SECP256K1_GENERATOR_POINT))
102
342
  // It is possible to switch SECP256K1_GENERATOR_POINT with some random point;
103
343
  // but it's too complex to prove.
104
344
  // Also used by bitcoin-core and bitcoinjs-lib
105
- export const TAPROOT_UNSPENDABLE_KEY: Bytes = sha256(Point.BASE.toBytes(false));
345
+ // This is the fixed BIP 341 H example, not the privacy-preserving H + rG variant.
346
+ // Downstream helpers use exact-byte equality with it to recognize
347
+ // library-generated script-only outputs.
348
+ /** Standard unspendable internal key used for script-only Taproot outputs. */
349
+ export const TAPROOT_UNSPENDABLE_KEY: TRet<Bytes> = /* @__PURE__ */ (() =>
350
+ sha256(Point.BASE.toBytes(false)) as TRet<Bytes>)();
106
351
 
352
+ /** Bitcoin network parameters. */
107
353
  export type BTC_NETWORK = {
354
+ /** Human-readable prefix used by Bech32 and Bech32m addresses. */
108
355
  bech32: string;
356
+ /** Base58 version byte for pay-to-public-key-hash addresses. */
109
357
  pubKeyHash: number;
358
+ /** Base58 version byte for pay-to-script-hash addresses. */
110
359
  scriptHash: number;
360
+ /** Base58 version byte for wallet-import-format private keys. */
111
361
  wif: number;
112
362
  };
113
- export const NETWORK: BTC_NETWORK = {
363
+ /** Bitcoin mainnet network parameters. */
364
+ export const NETWORK: BTC_NETWORK = /* @__PURE__ */ Object.freeze({
114
365
  bech32: 'bc',
115
366
  pubKeyHash: 0x00,
116
367
  scriptHash: 0x05,
117
368
  wif: 0x80,
118
- };
369
+ });
119
370
 
120
- export const TEST_NETWORK: BTC_NETWORK = {
371
+ /** Bitcoin testnet network parameters. */
372
+ export const TEST_NETWORK: BTC_NETWORK = /* @__PURE__ */ Object.freeze({
121
373
  bech32: 'tb',
122
374
  pubKeyHash: 0x6f,
123
375
  scriptHash: 0xc4,
124
376
  wif: 0xef,
125
- };
377
+ });
126
378
 
127
379
  // Exported for tests, internal method
128
- export function compareBytes(a: Bytes, b: Bytes): number {
129
- if (!isBytes(a) || !isBytes(b)) throw new Error(`cmp: wrong type a=${typeof a} b=${typeof b}`);
380
+ /**
381
+ * Lexicographically compares two byte arrays.
382
+ * @param a - first byte array
383
+ * @param b - second byte array
384
+ * @returns `-1`, `0`, or `1` depending on the ordering.
385
+ * @throws On wrong argument types. {@link TypeError}
386
+ * @example
387
+ * Compare two serialized keys using Bitcoin's byte ordering.
388
+ * ```ts
389
+ * compareBytes(new Uint8Array([1]), new Uint8Array([2]));
390
+ * ```
391
+ */
392
+ export function compareBytes(a: TArg<Bytes>, b: TArg<Bytes>): number {
393
+ if (!isBytes(a) || !isBytes(b))
394
+ throw new TypeError(`cmp: wrong type a=${typeof a} b=${typeof b}`);
130
395
  // -1 -> a<b, 0 -> a==b, 1 -> a>b
131
396
  const len = Math.min(a.length, b.length);
132
397
  for (let i = 0; i < len; i++) if (a[i] != b[i]) return Math.sign(a[i] - b[i]);
@@ -134,10 +399,23 @@ export function compareBytes(a: Bytes, b: Bytes): number {
134
399
  }
135
400
 
136
401
  // Reverses key<->values
402
+ /**
403
+ * Reverses an object's keys and values.
404
+ * @param obj - object to reverse
405
+ * @returns Object with original values mapped back to keys.
406
+ * @throws If duplicate values would collide while reversing the object. {@link Error}
407
+ * @example
408
+ * Flip a lookup table so the values become keys.
409
+ * ```ts
410
+ * reverseObject({ a: 1, b: 2 });
411
+ * ```
412
+ */
137
413
  export function reverseObject<T extends Record<string, string | number>>(
138
414
  obj: T
139
415
  ): { [K in T[keyof T]]: Extract<keyof T, string> } {
140
- const res = {} as any;
416
+ // Keep a raw dictionary shape so enum-like tables can reverse values like
417
+ // `toString` without colliding with inherited Object prototype properties.
418
+ const res = Object.create(null) as any;
141
419
  for (const k in obj) {
142
420
  if (res[obj[k]] !== undefined) throw new Error('duplicate key');
143
421
  res[obj[k]] = k;
@@ -145,4 +423,5 @@ export function reverseObject<T extends Record<string, string | number>>(
145
423
  return res;
146
424
  }
147
425
 
426
+ /** Union of all value types in an object type. */
148
427
  export type ValueOf<T> = T[keyof T];