hanzio 3.2.0 → 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.
- package/README.md +2 -1
- package/dist/cli/fields.d.ts +16 -6
- package/dist/cli/index.d.ts +57 -4
- package/dist/crypto/node.d.ts +17 -0
- package/dist/crypto/sealer.d.ts +34 -2
- package/dist/{index-nfvq3zax.js → index-818f7jz4.js} +228 -88
- package/dist/index-zz773050.js +135 -0
- package/dist/redis/concurrency-limit.d.ts +17 -7
- package/dist/src/cli/index.js +3 -1
- package/dist/src/crypto/index.js +15 -106
- package/dist/src/crypto/node.js +67 -0
- package/dist/src/i18n/cli.js +1 -1
- package/dist/src/redis/index.js +5 -1
- package/dist/src/testing/index.js +23 -4
- package/dist/string/index.d.ts +1 -1
- package/dist/testing/module-mocker.d.ts +9 -9
- package/dist/testing/prisma.d.ts +5 -1
- package/package.json +8 -2
- package/src/cli/README.md +63 -6
- package/src/cli/fields.ts +95 -38
- package/src/cli/index.ts +302 -91
- package/src/crypto/README.md +21 -7
- package/src/crypto/node.ts +90 -0
- package/src/crypto/sealer.ts +120 -62
- package/src/redis/README.md +9 -7
- package/src/redis/concurrency-limit.ts +26 -8
- package/src/string/index.ts +1 -1
- package/src/testing/README.md +12 -3
- package/src/testing/module-mocker.ts +40 -10
- package/src/testing/prisma.ts +6 -1
|
@@ -8,10 +8,19 @@ export type RedisConcurrencyLimitOptions = {
|
|
|
8
8
|
maxStartsPerSecond?: number;
|
|
9
9
|
failOpenAfterMs?: number;
|
|
10
10
|
commandTimeoutMs?: number;
|
|
11
|
-
/** Receives throttled warnings
|
|
12
|
-
logger?:
|
|
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 `
|
|
50
|
-
* customer host should
|
|
51
|
-
* or every key ever touched leaves a permanent
|
|
52
|
-
* such a limiter is discarded
|
|
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({
|
package/dist/src/cli/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CliError2,
|
|
3
|
+
CliUsageError2,
|
|
3
4
|
defineCommand2,
|
|
4
5
|
defineCli2
|
|
5
|
-
} from "../../index-
|
|
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
|
};
|
package/dist/src/crypto/index.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/src/i18n/cli.js
CHANGED
package/dist/src/redis/index.js
CHANGED
|
@@ -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
|
-
|
|
212
|
-
|
|
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
|
}
|
package/dist/string/index.d.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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(
|
|
18
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/dist/testing/prisma.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Mock, mock } from 'bun:test';
|
|
2
|
-
|
|
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()`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hanzio",
|
|
3
|
-
"version": "3.
|
|
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",
|
package/src/cli/README.md
CHANGED
|
@@ -6,7 +6,8 @@ description: Typed CLIs from Zod object schemas, parsed with node:util parseArgs
|
|
|
6
6
|
|
|
7
7
|
Each command's `input` is a Zod object: its keys become flags, Zod validates
|
|
8
8
|
them, and `run` gets the typed result. Parsing is `node:util` `parseArgs`, so
|
|
9
|
-
there is no extra dependency. Runs in Bun and Node 22.4
|
|
9
|
+
there is no extra dependency. Runs in Bun and Node 22.4+. Commands with an
|
|
10
|
+
`input` need `zod` 4; a CLI made only of `raw` commands loads without it.
|
|
10
11
|
|
|
11
12
|
```ts
|
|
12
13
|
// src/cli.ts
|
|
@@ -51,20 +52,27 @@ handles would keep the process alive.
|
|
|
51
52
|
| `z.string()`, `z.enum([...])` | `--name value` or `--name=value` |
|
|
52
53
|
| `z.number()` | `--repeats 3`, coerced before validation |
|
|
53
54
|
| `z.boolean()` | `--json`; `--no-json` sets `false`; missing is `false` |
|
|
55
|
+
| `noWatch: z.boolean()` | its own flag `--no-watch`, not `--watch` negated |
|
|
54
56
|
| `z.array(z.string())` | repeatable: `--model a --model b`; missing is `[]` |
|
|
57
|
+
| `z.union([z.boolean(), z.string()])` | `--expose` is `true`, `--expose=api,web` is the string |
|
|
55
58
|
| `.optional()`, `.default(x)` | may be left out |
|
|
56
59
|
| `.describe('…')` | help text |
|
|
57
60
|
| `.meta({ positional: true })` | positional, in key order; an array takes the rest |
|
|
58
|
-
| `.meta({ alias: 'r' })` | `-r value` |
|
|
59
|
-
| `.meta({ env: 'APP_REGION' })` | read from the environment when the flag is missing |
|
|
61
|
+
| `.meta({ alias: 'r' })` | `-r value`; longer aliases are flags: `alias: ['r', 'region-name']` |
|
|
62
|
+
| `.meta({ env: 'APP_REGION' })` | read from the environment when the flag is missing; a list is tried in order, and blank values count as unset |
|
|
60
63
|
|
|
61
64
|
Keys are camelCase in the schema and kebab-case on the command line (`dryRun`
|
|
62
65
|
→ `--dry-run`). These are the same `.meta()` keys as `trpc-cli`, plus `env`;
|
|
63
66
|
importing `hanzio/cli` types them on Zod's `GlobalMeta`.
|
|
64
67
|
Pipes read their input side, so `z.stringbool()` takes `--submit true`.
|
|
65
68
|
|
|
66
|
-
Every CLI gets `-h`/`--help` (root, groups and commands), `--version`
|
|
67
|
-
pass `version`, and "Did you mean …?" for mistyped commands and
|
|
69
|
+
Every CLI gets `-h`/`--help` (root, groups and commands), `--version` and
|
|
70
|
+
`-v` when you pass `version`, and "Did you mean …?" for mistyped commands and
|
|
71
|
+
flags. A root command named `version` also answers `--version` and `-v`, so it
|
|
72
|
+
decides the format (`tool v1.2.3`).
|
|
73
|
+
|
|
74
|
+
`defineCommand({ strict: false })` ignores unknown flags and extra
|
|
75
|
+
positionals instead of exiting 2.
|
|
68
76
|
|
|
69
77
|
## Commands
|
|
70
78
|
|
|
@@ -82,6 +90,30 @@ A command is one of:
|
|
|
82
90
|
- `defineCommand({ raw: true, run })`, which skips parsing: every argument
|
|
83
91
|
after the command name is in `ctx.argv` (for `tool prisma migrate dev`).
|
|
84
92
|
|
|
93
|
+
The first line of `description` is the command's summary in command lists;
|
|
94
|
+
its help shows all of it. `usage: '[name...] [--all]'` replaces the generated
|
|
95
|
+
arguments in both, and `examples: ['tool stop api']` adds an Examples section
|
|
96
|
+
to its help.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
defineCli({
|
|
100
|
+
name: 'evals',
|
|
101
|
+
defaultCommand: 'run', // `evals` and `evals --case x` run `evals run`
|
|
102
|
+
commands: { run, inspect }
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`defaultCommand` runs when no command name is given, or when the first
|
|
107
|
+
argument is a flag that isn't a global. Pass a function to read it from the
|
|
108
|
+
environment: `defaultCommand: (env) => env.RELEASE_COMMAND`.
|
|
109
|
+
|
|
110
|
+
`unknownCommand: (name, path) => node` resolves a name the group doesn't
|
|
111
|
+
have, for commands that come from config. Return `undefined` for the usual
|
|
112
|
+
"Unknown command" error.
|
|
113
|
+
|
|
114
|
+
`help: (text, { path }) => text` rewrites every help screen (`path` is `[]`
|
|
115
|
+
for the root), to add sections such as the tasks a config defines.
|
|
116
|
+
|
|
85
117
|
`run(input, ctx)` gets:
|
|
86
118
|
|
|
87
119
|
| `ctx` | |
|
|
@@ -118,7 +150,9 @@ const cli = defineCli({
|
|
|
118
150
|
})
|
|
119
151
|
```
|
|
120
152
|
|
|
121
|
-
Globals are accepted before or after the command names.
|
|
153
|
+
Globals are accepted before or after the command names. A positional global
|
|
154
|
+
comes before the first command name, so `globals: z.object({ module:
|
|
155
|
+
z.string().meta({ positional: true }) })` reads `tool ./app.ts list`. `context` runs after
|
|
122
156
|
parsing, so `--help` never connects anywhere. `guard` runs before every
|
|
123
157
|
command. A command defined in its own file types its context by annotating
|
|
124
158
|
`ctx`:
|
|
@@ -151,6 +185,29 @@ message is printed without a stack. Other errors print their stack. With
|
|
|
151
185
|
Run "better-mastra-evals run --help" for usage.
|
|
152
186
|
```
|
|
153
187
|
|
|
188
|
+
`onError(error, io)` sees every error first. Return the exit code, or
|
|
189
|
+
`undefined` for the default output; `io.print()` writes that output and
|
|
190
|
+
returns its code. Usage errors are `CliUsageError`s with a `kind`
|
|
191
|
+
(`missing-command`, `unknown-command`, `invalid-arguments`), the command
|
|
192
|
+
`path`, and the `help` for it:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
defineCli({
|
|
196
|
+
name: 'tool',
|
|
197
|
+
commands,
|
|
198
|
+
onError: (error, io) => {
|
|
199
|
+
// Help instead of an error when run without a command.
|
|
200
|
+
if (error instanceof CliUsageError && error.kind === 'missing-command') {
|
|
201
|
+
io.stdout.write(error.help)
|
|
202
|
+
return 0
|
|
203
|
+
}
|
|
204
|
+
// Keep the message, but exit 1 on bad arguments.
|
|
205
|
+
const code = io.print()
|
|
206
|
+
return code === 2 ? 1 : code
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
154
211
|
## Shell completion
|
|
155
212
|
|
|
156
213
|
A CLI with commands also gets `tool completion bash` and `tool completion
|