hanzio 3.1.1 → 3.3.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 (39) hide show
  1. package/README.md +8 -1
  2. package/dist/cli/fields.d.ts +16 -6
  3. package/dist/cli/index.d.ts +57 -4
  4. package/dist/crypto/node.d.ts +17 -0
  5. package/dist/crypto/sealer.d.ts +34 -2
  6. package/dist/format/index.d.ts +6 -3
  7. package/dist/{index-nfvq3zax.js → index-818f7jz4.js} +228 -88
  8. package/dist/index-zz773050.js +135 -0
  9. package/dist/redis/concurrency-limit.d.ts +17 -7
  10. package/dist/src/cli/index.js +3 -1
  11. package/dist/src/crypto/index.js +15 -106
  12. package/dist/src/crypto/node.js +67 -0
  13. package/dist/src/format/index.js +4 -3
  14. package/dist/src/i18n/cli.js +33 -1
  15. package/dist/src/redis/index.js +5 -1
  16. package/dist/src/testing/index.js +23 -4
  17. package/dist/string/index.d.ts +1 -1
  18. package/dist/testing/module-mocker.d.ts +9 -9
  19. package/dist/testing/prisma.d.ts +5 -1
  20. package/dist/typedswitch/index.d.ts +26 -1
  21. package/package.json +8 -2
  22. package/src/cli/README.md +63 -6
  23. package/src/cli/fields.ts +95 -38
  24. package/src/cli/index.ts +302 -91
  25. package/src/crypto/README.md +21 -7
  26. package/src/crypto/node.ts +90 -0
  27. package/src/crypto/sealer.ts +120 -62
  28. package/src/format/README.md +7 -0
  29. package/src/format/index.ts +11 -5
  30. package/src/i18n/README.md +3 -1
  31. package/src/i18n/cli.ts +40 -0
  32. package/src/redis/README.md +9 -7
  33. package/src/redis/concurrency-limit.ts +26 -8
  34. package/src/string/index.ts +1 -1
  35. package/src/testing/README.md +12 -3
  36. package/src/testing/module-mocker.ts +40 -10
  37. package/src/testing/prisma.ts +6 -1
  38. package/src/typedswitch/README.md +20 -9
  39. package/src/typedswitch/index.ts +84 -8
@@ -8,10 +8,19 @@ export type RedisConcurrencyLimitOptions = {
8
8
  maxStartsPerSecond?: number;
9
9
  failOpenAfterMs?: number;
10
10
  commandTimeoutMs?: number;
11
- /** Receives throttled warnings. Defaults to `console`. */
12
- logger?: Pick<Console, 'warn'>;
11
+ /** Receives throttled warnings with `{ key, error }`. Defaults to `console`. */
12
+ logger?: {
13
+ warn(message: string, attributes: Record<string, unknown>): void;
14
+ };
15
+ /** The label `onWait` and `onQueueDepth` report under. Default: `key`. */
16
+ metricLabel?: string;
13
17
  /** Called when a caller starts queueing; the returned function runs once it stops. */
14
- onWait?: (stage: ConcurrencyLimitStage) => (() => void) | void;
18
+ onWait?: (stage: ConcurrencyLimitStage, metricLabel: string) => (() => void) | void;
19
+ /**
20
+ * Called once with `queueDepth`, to register a gauge; the returned function
21
+ * runs on `dispose()`.
22
+ */
23
+ onQueueDepth?: (queueDepth: () => number, metricLabel: string) => (() => void) | void;
15
24
  };
16
25
  export type RedisConcurrencyLimit = {
17
26
  <T>(run: () => Promise<T>, signal?: AbortSignal): Promise<T>;
@@ -46,10 +55,11 @@ export type RedisConcurrencyLimit = {
46
55
  * That verdict is shared per limiter: once tripped, later calls degrade
47
56
  * immediately while a background probe watches for Redis to come back.
48
57
  *
49
- * `onWait` and `queueDepth` feed metrics. A limiter minted per tenant or per
50
- * customer host should report under a shared pool label rather than its key,
51
- * or every key ever touched leaves a permanent series. Call `dispose()` when
52
- * such a limiter is discarded, and unregister any gauge reading `queueDepth`.
58
+ * `onWait` and `onQueueDepth` feed metrics under `metricLabel`. A limiter
59
+ * minted per tenant or per customer host should share one pool label rather
60
+ * than default to its key, or every key ever touched leaves a permanent
61
+ * series. Call `dispose()` when such a limiter is discarded; it also runs the
62
+ * unregister function `onQueueDepth` returned.
53
63
  *
54
64
  * @example
55
65
  * const withLimit = createRedisConcurrencyLimit({
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  CliError2,
3
+ CliUsageError2,
3
4
  defineCommand2,
4
5
  defineCli2
5
- } from "../../index-nfvq3zax.js";
6
+ } from "../../index-818f7jz4.js";
6
7
  export {
7
8
  CliError2 as CliError,
9
+ CliUsageError2 as CliUsageError,
8
10
  defineCli2 as defineCli,
9
11
  defineCommand2 as defineCommand
10
12
  };
@@ -1,3 +1,11 @@
1
+ import {
2
+ SealerError2,
3
+ sealerKeyInputs2,
4
+ formatEnvelope2,
5
+ parseEnvelope2,
6
+ jsonToSeal2,
7
+ createSealer2
8
+ } from "../../index-zz773050.js";
1
9
  import {
2
10
  utf8Encode2,
3
11
  utf8Decode2,
@@ -7,117 +15,14 @@ import {
7
15
  decodeBase64Url2,
8
16
  bytesToHex2,
9
17
  hexToBytes2,
10
- toBytes,
11
18
  safeEqual2,
12
19
  randomBytes2,
13
20
  randomToken2,
14
21
  sha2562,
15
22
  sha256Hex2,
16
23
  hmacSign2,
17
- verifyHmacSignature2,
18
- getSubtleCrypto
24
+ verifyHmacSignature2
19
25
  } from "../../index-rp5pqghz.js";
20
- // src/crypto/sealer.ts
21
- var ERROR_MESSAGES = {
22
- malformed: "Malformed sealed value",
23
- unsupported_version: "Unsupported sealed value version",
24
- decrypt_failed: "Could not open sealed value: wrong key or tampered data"
25
- };
26
-
27
- class SealerError extends Error {
28
- code;
29
- constructor(code) {
30
- super(ERROR_MESSAGES[code]);
31
- this.name = "SealerError";
32
- this.code = code;
33
- }
34
- }
35
- var PREFIX = "enc:v1:";
36
- var IV_BYTES = 12;
37
- var TAG_BYTES = 16;
38
- var DEFAULT_INFO = "hanzio.sealer.aes-256-gcm.v1";
39
- function createSealer(options) {
40
- const { secret, salt = "", info = DEFAULT_INFO } = options;
41
- const secretBytes = toBytes(secret);
42
- if (secretBytes.length === 0) {
43
- throw new TypeError("Sealer secret must not be empty");
44
- }
45
- let keyPromise;
46
- const getKey = () => {
47
- if (!keyPromise) {
48
- keyPromise = deriveKey(secretBytes, salt, info);
49
- keyPromise.catch(() => {
50
- keyPromise = undefined;
51
- });
52
- }
53
- return keyPromise;
54
- };
55
- const seal = async (plaintext) => {
56
- const iv = randomBytes2(IV_BYTES);
57
- const encrypted = new Uint8Array(await getSubtleCrypto().encrypt({ name: "AES-GCM", iv }, await getKey(), utf8Encode2(plaintext)));
58
- const tagStart = encrypted.length - TAG_BYTES;
59
- return `${PREFIX}${encodeBase64Url2(iv)}.${encodeBase64Url2(encrypted.subarray(tagStart))}.${encodeBase64Url2(encrypted.subarray(0, tagStart))}`;
60
- };
61
- const open = async (sealed) => {
62
- if (typeof sealed !== "string" || !sealed.startsWith(PREFIX)) {
63
- const isOtherVersion = typeof sealed === "string" && /^enc:v\d+:/.test(sealed);
64
- throw new SealerError(isOtherVersion ? "unsupported_version" : "malformed");
65
- }
66
- const parts = sealed.slice(PREFIX.length).split(".");
67
- let iv;
68
- let tag;
69
- let ciphertext;
70
- try {
71
- if (parts.length !== 3)
72
- throw new Error;
73
- iv = decodeBase64Url2(parts[0]);
74
- tag = decodeBase64Url2(parts[1]);
75
- ciphertext = decodeBase64Url2(parts[2]);
76
- } catch (_error) {
77
- throw new SealerError("malformed");
78
- }
79
- if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) {
80
- throw new SealerError("malformed");
81
- }
82
- const encrypted = new Uint8Array(ciphertext.length + TAG_BYTES);
83
- encrypted.set(ciphertext);
84
- encrypted.set(tag, ciphertext.length);
85
- const key = await getKey();
86
- let plaintext;
87
- try {
88
- plaintext = await getSubtleCrypto().decrypt({ name: "AES-GCM", iv }, key, encrypted);
89
- } catch (_error) {
90
- throw new SealerError("decrypt_failed");
91
- }
92
- return utf8Decode2(plaintext);
93
- };
94
- return {
95
- seal,
96
- open,
97
- async sealJson(value) {
98
- const json = JSON.stringify(value);
99
- if (json === undefined) {
100
- throw new TypeError("sealJson value is not JSON-serializable");
101
- }
102
- return seal(json);
103
- },
104
- async openJson(sealed) {
105
- return JSON.parse(await open(sealed));
106
- }
107
- };
108
- }
109
- async function deriveKey(secret, salt, info) {
110
- const subtle = getSubtleCrypto();
111
- const baseKey = await subtle.importKey("raw", secret, "HKDF", false, [
112
- "deriveKey"
113
- ]);
114
- return subtle.deriveKey({
115
- name: "HKDF",
116
- hash: "SHA-256",
117
- salt: utf8Encode2(salt),
118
- info: utf8Encode2(info)
119
- }, baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
120
- }
121
26
  // src/crypto/webhook.ts
122
27
  var TIMESTAMP = /^\d+$/;
123
28
  async function verifySvixSignature(options) {
@@ -174,18 +79,22 @@ function readHeader(headers, name) {
174
79
  return;
175
80
  }
176
81
  export {
177
- SealerError,
82
+ SealerError2 as SealerError,
178
83
  bytesToHex2 as bytesToHex,
179
- createSealer,
84
+ createSealer2 as createSealer,
180
85
  decodeBase642 as decodeBase64,
181
86
  decodeBase64Url2 as decodeBase64Url,
182
87
  encodeBase642 as encodeBase64,
183
88
  encodeBase64Url2 as encodeBase64Url,
89
+ formatEnvelope2 as formatEnvelope,
184
90
  hexToBytes2 as hexToBytes,
185
91
  hmacSign2 as hmacSign,
92
+ jsonToSeal2 as jsonToSeal,
93
+ parseEnvelope2 as parseEnvelope,
186
94
  randomBytes2 as randomBytes,
187
95
  randomToken2 as randomToken,
188
96
  safeEqual2 as safeEqual,
97
+ sealerKeyInputs2 as sealerKeyInputs,
189
98
  sha2562 as sha256,
190
99
  sha256Hex2 as sha256Hex,
191
100
  utf8Decode2 as utf8Decode,
@@ -0,0 +1,67 @@
1
+ import {
2
+ SealerError2,
3
+ sealerKeyInputs2,
4
+ formatEnvelope2,
5
+ parseEnvelope2,
6
+ jsonToSeal2
7
+ } from "../../index-zz773050.js";
8
+
9
+ // src/crypto/node.ts
10
+ import {
11
+ createCipheriv,
12
+ createDecipheriv,
13
+ hkdfSync,
14
+ randomBytes
15
+ } from "node:crypto";
16
+ var ALGORITHM = "aes-256-gcm";
17
+ function createSealerSync(options) {
18
+ const { secret, contexts } = sealerKeyInputs2(options);
19
+ const keys = [];
20
+ const getKey = (index) => {
21
+ let key = keys[index];
22
+ if (!key) {
23
+ const { salt, info } = contexts[index];
24
+ key = Buffer.from(hkdfSync("sha256", secret, salt, info, 32));
25
+ keys[index] = key;
26
+ }
27
+ return key;
28
+ };
29
+ const seal = (plaintext) => {
30
+ const iv = randomBytes(12);
31
+ const cipher = createCipheriv(ALGORITHM, getKey(0), iv);
32
+ const ciphertext = Buffer.concat([
33
+ cipher.update(plaintext, "utf8"),
34
+ cipher.final()
35
+ ]);
36
+ return formatEnvelope2(iv, cipher.getAuthTag(), ciphertext);
37
+ };
38
+ const openEnvelope = ({ iv, tag, ciphertext }) => {
39
+ for (let index = 0;index < contexts.length; index++) {
40
+ try {
41
+ const decipher = createDecipheriv(ALGORITHM, getKey(index), iv);
42
+ decipher.setAuthTag(tag);
43
+ return Buffer.concat([
44
+ decipher.update(ciphertext),
45
+ decipher.final()
46
+ ]).toString("utf8");
47
+ } catch (_error) {}
48
+ }
49
+ throw new SealerError2("decrypt_failed");
50
+ };
51
+ return {
52
+ seal,
53
+ open(sealed) {
54
+ const envelope = parseEnvelope2(sealed, options.passthroughUnsealed);
55
+ return envelope ? openEnvelope(envelope) : sealed;
56
+ },
57
+ sealJson: (value) => seal(jsonToSeal2(value)),
58
+ openJson(sealed) {
59
+ const envelope = parseEnvelope2(sealed, options.passthroughUnsealed);
60
+ return envelope ? JSON.parse(openEnvelope(envelope)) : sealed;
61
+ }
62
+ };
63
+ }
64
+ export {
65
+ SealerError2 as SealerError,
66
+ createSealerSync
67
+ };
@@ -193,8 +193,9 @@ var DATE_PRESETS = {
193
193
  "dddd, MMMM D": { weekday: "long", day: "numeric", month: "long" },
194
194
  "MMMM YYYY": { month: "long", year: "numeric" }
195
195
  };
196
- function formatDate(date, preset, locale) {
197
- return getDateTimeFormat(locale, DATE_PRESETS[preset]).format(toTime(date));
196
+ function formatDate(date, format, locale) {
197
+ const options = typeof format === "string" ? DATE_PRESETS[format] : format;
198
+ return getDateTimeFormat(locale, options).format(toTime(date));
198
199
  }
199
200
  function formatDateTime(date, locale) {
200
201
  return getDateTimeFormat(locale, {
@@ -249,7 +250,7 @@ function pluralize(count, forms, locale) {
249
250
  }
250
251
  function withLocale(getLocale) {
251
252
  return {
252
- formatDate: (date, preset) => formatDate(date, preset, getLocale()),
253
+ formatDate: (date, format) => formatDate(date, format, getLocale()),
253
254
  formatDateTime: (date) => formatDateTime(date, getLocale()),
254
255
  formatCompactTime: (date) => formatCompactTime(date, getLocale()),
255
256
  formatDayDivider: (date) => formatDayDivider(date, getLocale()),
@@ -3,7 +3,7 @@ import {
3
3
  CliError2,
4
4
  defineCommand2,
5
5
  defineCli2
6
- } from "../../index-nfvq3zax.js";
6
+ } from "../../index-818f7jz4.js";
7
7
  import"../../index-najbyzpr.js";
8
8
  import {
9
9
  validateCatalogs2,
@@ -12,6 +12,7 @@ import {
12
12
 
13
13
  // src/i18n/cli.ts
14
14
  import { existsSync } from "node:fs";
15
+ import * as nodeModule from "node:module";
15
16
  import { dirname, resolve } from "node:path";
16
17
  import { pathToFileURL } from "node:url";
17
18
  import { z } from "zod";
@@ -21,10 +22,41 @@ var CONFIG_NAMES = [
21
22
  "i18n.config.js",
22
23
  "i18n.config.mjs"
23
24
  ];
25
+ var BUNDLER_SUFFIXES = [
26
+ ".ts",
27
+ ".mts",
28
+ ".js",
29
+ ".mjs",
30
+ "/index.ts",
31
+ "/index.mts",
32
+ "/index.js",
33
+ "/index.mjs"
34
+ ];
35
+ function resolveLikeBundlers() {
36
+ nodeModule.registerHooks?.({
37
+ resolve(specifier, context, nextResolve) {
38
+ try {
39
+ return nextResolve(specifier, context);
40
+ } catch (error) {
41
+ const code = error.code;
42
+ const retry = specifier.startsWith(".") && (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNSUPPORTED_DIR_IMPORT");
43
+ if (!retry)
44
+ throw error;
45
+ for (const suffix of BUNDLER_SUFFIXES) {
46
+ try {
47
+ return nextResolve(specifier + suffix, context);
48
+ } catch {}
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+ });
54
+ }
24
55
  async function loadConfig(file) {
25
56
  const path = file ? resolve(file) : CONFIG_NAMES.map((name) => resolve(name)).find((path) => existsSync(path));
26
57
  if (!path || !existsSync(path))
27
58
  throw new CliError2(`No i18n config found: pass --config or add ${CONFIG_NAMES[0]}.`);
59
+ resolveLikeBundlers();
28
60
  const loaded = await import(pathToFileURL(path).href);
29
61
  if (!loaded.default?.catalogs)
30
62
  throw new CliError2(`${path} must default-export an i18n config`);
@@ -315,6 +315,7 @@ function createRedisConcurrencyLimit(input) {
315
315
  if (!Number.isInteger(input.max) || input.max < 1)
316
316
  throw new Error(`max must be a positive integer, got ${input.max}`);
317
317
  const logger = input.logger ?? console;
318
+ const metricLabel = input.metricLabel ?? input.key;
318
319
  const commandTimeoutMs = input.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
319
320
  const send = withCommandTimeout(input.send, commandTimeoutMs);
320
321
  const ttlMs = String(input.slotTtlMs);
@@ -435,7 +436,7 @@ function createRedisConcurrencyLimit(input) {
435
436
  return {
436
437
  depth: () => queue.length,
437
438
  acquire(token) {
438
- const stopWait = input.onWait?.(stage);
439
+ const stopWait = input.onWait?.(stage, metricLabel);
439
440
  const granted = new Promise((resolve) => {
440
441
  queue.push({ token, settle: resolve });
441
442
  });
@@ -451,6 +452,7 @@ function createRedisConcurrencyLimit(input) {
451
452
  const slotPump = createPump("slot", input.max, async (tokens) => parseGrantReply(await acquireScript([inflightKey, cooldownKey], [String(input.max), ttlMs, ...tokens])));
452
453
  const startPump = maxStartsPerSecond === undefined ? null : createPump("start", maxStartsPerSecond, async (tokens) => parseGrantReply(await startScript([startsKey, cooldownKey], [String(maxStartsPerSecond), String(START_WINDOW_MS), ...tokens])));
453
454
  const queueDepth = () => slotPump.depth() + (startPump?.depth() ?? 0);
455
+ let unregisterQueueDepth = input.onQueueDepth?.(queueDepth, metricLabel);
454
456
  const heldTokens = new Set;
455
457
  let heartbeatTimer = null;
456
458
  function scheduleHeartbeat() {
@@ -555,6 +557,8 @@ function createRedisConcurrencyLimit(input) {
555
557
  }
556
558
  }
557
559
  function dispose() {
560
+ unregisterQueueDepth?.();
561
+ unregisterQueueDepth = undefined;
558
562
  if (probeTimer) {
559
563
  clearTimeout(probeTimer);
560
564
  probeTimer = null;
@@ -205,11 +205,30 @@ function mockCallArg(fn, callIndex = 0, argIndex = 0) {
205
205
  return fn.mock.calls[callIndex]?.[argIndex];
206
206
  }
207
207
  // src/testing/module-mocker.ts
208
+ import { isAbsolute } from "node:path";
209
+ function resolveFrom(baseDir) {
210
+ return (specifier) => {
211
+ if (isAbsolute(specifier))
212
+ return specifier;
213
+ if (specifier.startsWith("."))
214
+ return Bun.resolveSync(specifier, baseDir);
215
+ try {
216
+ return Bun.resolveSync(specifier, baseDir);
217
+ } catch (error) {
218
+ try {
219
+ return Bun.resolveSync(`./${specifier}`, baseDir);
220
+ } catch {
221
+ throw error;
222
+ }
223
+ }
224
+ };
225
+ }
226
+
208
227
  class ModuleMocker {
209
- resolve;
210
228
  restorers = [];
211
- constructor(resolve = (specifier) => specifier) {
212
- this.resolve = resolve;
229
+ resolve;
230
+ constructor(baseDirOrResolve = (specifier) => specifier) {
231
+ this.resolve = typeof baseDirOrResolve === "string" ? resolveFrom(baseDirOrResolve) : baseDirOrResolve;
213
232
  }
214
233
  async mock(specifier, renderMocks) {
215
234
  const { mock } = await import("bun:test");
@@ -222,7 +241,7 @@ class ModuleMocker {
222
241
  });
223
242
  }
224
243
  clear() {
225
- for (const restore of this.restorers)
244
+ for (const restore of this.restorers.toReversed())
226
245
  restore();
227
246
  this.restorers = [];
228
247
  }
@@ -3,7 +3,7 @@ export type JsonValue = string | number | boolean | null | JsonObject | JsonArra
3
3
  export type JsonObject = {
4
4
  [key: string]: JsonValue | undefined;
5
5
  };
6
- export type JsonArray = Array<JsonValue | undefined>;
6
+ export type JsonArray = JsonValue[];
7
7
  /**
8
8
  * Parses the first number-like token in a string. Only a `-` directly before
9
9
  * the digits is a sign, so `'2024-01-05'` gives `2024`. Whitespace and the
@@ -9,21 +9,21 @@ type ModuleExports = Record<string, unknown>;
9
9
  * keep working, and `clear()` re-registers the originals. Call it in
10
10
  * `afterEach` or `afterAll`.
11
11
  *
12
- * Bun resolves specifiers relative to the file that registers the mock, so
13
- * pass absolute paths or package names. A `resolve` function maps your own
14
- * shorthand (an `@/` alias, a source-root-relative path) to absolute paths.
12
+ * Without an argument, pass absolute paths. Pass your source root to resolve
13
+ * against it: package names from there (so mocks hit the same copy your code
14
+ * imports), and `./lib/x` or `lib/x` relative to it. A function maps your own
15
+ * shorthand, such as an `@/` alias, to absolute paths instead.
15
16
  *
16
17
  * @example
17
- * const moduleMocker = new ModuleMocker((specifier) =>
18
- * specifier.startsWith('@/') ? path.resolve(srcDir, specifier.slice(2)) : specifier
19
- * )
20
- * await moduleMocker.mock('@/lib/posthog', () => ({ captureException: mock() }))
18
+ * const moduleMocker = new ModuleMocker(srcDir)
19
+ * await moduleMocker.mock('lib/posthog.ts', () => ({ captureException: mock() }))
20
+ * await moduleMocker.mock('ai', () => ({ generateText: mock() }))
21
21
  * afterAll(() => moduleMocker.clear())
22
22
  */
23
23
  export declare class ModuleMocker {
24
- private readonly resolve;
25
24
  private restorers;
26
- constructor(resolve?: (specifier: string) => string);
25
+ private readonly resolve;
26
+ constructor(baseDirOrResolve?: string | ((specifier: string) => string));
27
27
  mock(specifier: string, renderMocks: () => ModuleExports): Promise<void>;
28
28
  clear(): void;
29
29
  }
@@ -1,5 +1,9 @@
1
1
  import type { Mock, mock } from 'bun:test';
2
- export type PrismaMethodMock = Mock<(...args: unknown[]) => Promise<unknown>>;
2
+ /**
3
+ * `never[]` parameters let `mockImplementation` take a callback that types its
4
+ * arguments; the plain call signature keeps the mock callable with any.
5
+ */
6
+ export type PrismaMethodMock = Mock<(...args: never[]) => Promise<unknown>> & ((...args: unknown[]) => Promise<unknown>);
3
7
  /**
4
8
  * A Prisma client where every model delegate and every method is a Bun mock,
5
9
  * created on first access, like `jest-mock-extended`'s `mockDeep()`.
@@ -80,6 +80,23 @@ type RawHandlerReturnType<T> = T extends (...args: any[]) => infer R ? R : never
80
80
  type RawCasesReturnType<Cases> = {
81
81
  [K in keyof Cases]: RawHandlerReturnType<Cases[K]>;
82
82
  }[keyof Cases];
83
+ /**
84
+ * `R` is only inferred from the call's contextual type. Without one it stays
85
+ * `unknown`, and the overload returns the handlers' own return types.
86
+ */
87
+ type Annotated<R, Inferred> = unknown extends R ? Inferred : R;
88
+ /**
89
+ * TypeScript fixes a handler's return type the first time it checks it, and
90
+ * an object-mode call (`value, key, cases`) is first tried against the
91
+ * plain-value overload with a default (`value, cases, defaultCase`). This type
92
+ * types those cases' handlers by the annotation there too, so they keep their
93
+ * literals. No value satisfies it: `default` would have to be `never`.
94
+ */
95
+ type ObjectCasesContext<R> = {
96
+ [key: string]: (value: never) => R;
97
+ } & {
98
+ default: never;
99
+ };
83
100
  /** Only required, defined handlers guarantee that a case is handled. */
84
101
  type HandledCaseKeys<Cases> = {
85
102
  [K in keyof Cases]-?: {} extends Pick<Cases, K> ? never : undefined extends Cases[K] ? never : K;
@@ -113,13 +130,21 @@ export type PartialObjectSwitchCases<T, K extends keyof T, R> = T[K] extends Dis
113
130
  type NoUnknownCases<Cases, Allowed extends string> = {
114
131
  [P in keyof Cases as `${P & (string | number)}` extends Allowed ? never : P]: never;
115
132
  };
133
+ export declare function typedSwitch<T extends Discriminant, R, Cases extends {
134
+ [V in T as `${V}`]: (value: V) => R;
135
+ } & NoUnknownCases<Cases, `${T}`>>(value: T, cases: Cases): Annotated<R, RawCasesReturnType<Cases>>;
136
+ export declare function typedSwitch<T extends Discriminant, R, Cases extends {
137
+ [V in T as `${V}`]?: (value: V) => R;
138
+ } & NoUnknownCases<Cases, `${T}`>, DefaultCase extends ((value: Exclude<T, ValuesForKeys<T, HandledCaseKeys<Cases>>>) => R) | ObjectCasesContext<R>>(value: T, cases: Cases, defaultCase: DefaultCase): Annotated<R, RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>>;
139
+ export declare function typedSwitch<T extends object, K extends keyof T, R, Cases extends ObjectCases<T, K, R> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>>(value: T, key: K, cases: Cases): Annotated<R, RawCasesReturnType<Cases>>;
140
+ export declare function typedSwitch<T extends object, K extends keyof T, R, Cases extends PartialObjectCases<T, K, R> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>, DefaultCase extends (value: Exclude<T, Record<K, ValuesForKeys<T[K], HandledCaseKeys<Cases>>>>) => R>(value: T, key: K, cases: Cases, defaultCase: DefaultCase): Annotated<R, RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>>;
116
141
  export declare function typedSwitch<T extends Discriminant, Cases extends {
117
142
  [V in T as `${V}`]: (value: V) => unknown;
118
143
  } & NoUnknownCases<Cases, `${T}`>>(value: T, cases: Cases): RawCasesReturnType<Cases>;
144
+ export declare function typedSwitch<T extends object, K extends keyof T, Cases extends ObjectCases<T, K, unknown> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>>(value: T, key: K, cases: Cases): RawCasesReturnType<Cases>;
119
145
  export declare function typedSwitch<T extends Discriminant, Cases extends {
120
146
  [V in T as `${V}`]?: (value: V) => unknown;
121
147
  } & NoUnknownCases<Cases, `${T}`>, DefaultCase extends (value: Exclude<T, ValuesForKeys<T, HandledCaseKeys<Cases>>>) => unknown>(value: T, cases: Cases, defaultCase: DefaultCase): RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>;
122
- export declare function typedSwitch<T extends object, K extends keyof T, Cases extends ObjectCases<T, K, unknown> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>>(value: T, key: K, cases: Cases): RawCasesReturnType<Cases>;
123
148
  export declare function typedSwitch<T extends object, K extends keyof T, Cases extends PartialObjectCases<T, K, unknown> & NoUnknownCases<Cases, `${Extract<T[K], Discriminant>}`>, DefaultCase extends (value: Exclude<T, Record<K, ValuesForKeys<T[K], HandledCaseKeys<Cases>>>>) => unknown>(value: T, key: K, cases: Cases, defaultCase: DefaultCase): RawCasesReturnType<Cases> | RawHandlerReturnType<DefaultCase>;
124
149
  /**
125
150
  * Builds cases that share one handler, for spreading into `typedSwitch` cases.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hanzio",
3
- "version": "3.1.1",
3
+ "version": "3.3.0",
4
4
  "description": "A lightweight set of utility functions for TypeScript",
5
5
  "type": "module",
6
6
  "module": "./dist/src/index.js",
@@ -107,6 +107,12 @@
107
107
  "import": "./dist/src/crypto/index.js",
108
108
  "default": "./dist/src/crypto/index.js"
109
109
  },
110
+ "./crypto/node": {
111
+ "types": "./dist/crypto/node.d.ts",
112
+ "bun": "./src/crypto/node.ts",
113
+ "import": "./dist/src/crypto/node.js",
114
+ "default": "./dist/src/crypto/node.js"
115
+ },
110
116
  "./storage": {
111
117
  "types": "./dist/storage/index.d.ts",
112
118
  "bun": "./src/storage/index.ts",
@@ -194,7 +200,7 @@
194
200
  },
195
201
  "scripts": {
196
202
  "build": "rm -rf dist && bun run build:js && bun run build:types",
197
- "build:js": "bun build ./src/index.ts ./src/zod/index.ts ./src/api-wrapper/index.ts ./src/api-wrapper/testing.ts ./src/api-wrapper/connection-auth.ts ./src/jwt/index.ts ./src/sitemap/index.ts ./src/p-queue/index.ts ./src/cool-console-log/index.ts ./src/format/index.ts ./src/format/phone.ts ./src/crypto/index.ts ./src/storage/index.ts ./src/i18n/index.ts ./src/i18n/vue.ts ./src/i18n/i18next.ts ./src/i18n/vite.ts ./src/i18n/check.ts ./src/i18n/cli.ts ./src/iso/index.ts ./src/secrets/index.ts ./src/secrets/vite.ts ./src/secrets/worker.ts ./src/redis/index.ts ./src/http/index.ts ./src/cli/index.ts ./src/testing/index.ts --outdir ./dist --target node --packages external --splitting",
203
+ "build:js": "bun build ./src/index.ts ./src/zod/index.ts ./src/api-wrapper/index.ts ./src/api-wrapper/testing.ts ./src/api-wrapper/connection-auth.ts ./src/jwt/index.ts ./src/sitemap/index.ts ./src/p-queue/index.ts ./src/cool-console-log/index.ts ./src/format/index.ts ./src/format/phone.ts ./src/crypto/index.ts ./src/crypto/node.ts ./src/storage/index.ts ./src/i18n/index.ts ./src/i18n/vue.ts ./src/i18n/i18next.ts ./src/i18n/vite.ts ./src/i18n/check.ts ./src/i18n/cli.ts ./src/iso/index.ts ./src/secrets/index.ts ./src/secrets/vite.ts ./src/secrets/worker.ts ./src/redis/index.ts ./src/http/index.ts ./src/cli/index.ts ./src/testing/index.ts --outdir ./dist --target node --packages external --splitting",
198
204
  "build:types": "tsgo -p tsconfig.build.json",
199
205
  "prepublishOnly": "bun run build",
200
206
  "test": "bun test",