@rezwanul7/secret-vault 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +242 -2
- package/dist/index.cjs +415 -0
- package/dist/index.d.cts +222 -0
- package/dist/index.d.ts +222 -0
- package/dist/index.js +410 -0
- package/package.json +54 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// src/encoding.ts
|
|
2
|
+
var encoder = new TextEncoder();
|
|
3
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
4
|
+
function utf8Encode(text) {
|
|
5
|
+
return encoder.encode(text);
|
|
6
|
+
}
|
|
7
|
+
function utf8Decode(bytes) {
|
|
8
|
+
return decoder.decode(bytes);
|
|
9
|
+
}
|
|
10
|
+
function base64urlEncode(bytes) {
|
|
11
|
+
let binary = "";
|
|
12
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
13
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
14
|
+
}
|
|
15
|
+
function base64urlDecode(text) {
|
|
16
|
+
if (!/^[A-Za-z0-9_-]*$/.test(text) || text.length % 4 === 1) return void 0;
|
|
17
|
+
const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
|
|
18
|
+
const bytes = new Uint8Array(binary.length);
|
|
19
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
20
|
+
return base64urlEncode(bytes) === text ? bytes : void 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/errors.ts
|
|
24
|
+
var VaultError = class extends Error {
|
|
25
|
+
/** A stable, machine-readable code such as `VAULT_CONFIG_INVALID`. */
|
|
26
|
+
code;
|
|
27
|
+
/**
|
|
28
|
+
* @param message - The error message.
|
|
29
|
+
* @param code - The stable error code.
|
|
30
|
+
* @param options - The underlying `cause`, if any.
|
|
31
|
+
*/
|
|
32
|
+
constructor(message, code, options) {
|
|
33
|
+
super(message, options);
|
|
34
|
+
this.name = "VaultError";
|
|
35
|
+
this.code = code;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var VaultConfigError = class extends VaultError {
|
|
39
|
+
/**
|
|
40
|
+
* @param message - What is wrong, naming the option but never its value.
|
|
41
|
+
* @param options - The underlying `cause`, if any.
|
|
42
|
+
*/
|
|
43
|
+
constructor(message, options) {
|
|
44
|
+
super(message, "VAULT_CONFIG_INVALID", options);
|
|
45
|
+
this.name = "VaultConfigError";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var VaultDecryptError = class extends VaultError {
|
|
49
|
+
/** Why the Sealed value couldn't be opened. */
|
|
50
|
+
reason;
|
|
51
|
+
/**
|
|
52
|
+
* The Context the Sealed value was opened under, or `undefined` when the
|
|
53
|
+
* method takes no Context (`needsRotation`).
|
|
54
|
+
*/
|
|
55
|
+
context;
|
|
56
|
+
/**
|
|
57
|
+
* @param reason - Why the Sealed value couldn't be opened.
|
|
58
|
+
* @param context - The Context it was opened under, or `undefined` when
|
|
59
|
+
* there is none.
|
|
60
|
+
* @param options - The underlying `cause`, if any.
|
|
61
|
+
*/
|
|
62
|
+
constructor(reason, context, options) {
|
|
63
|
+
super(
|
|
64
|
+
context === void 0 ? `Could not read Sealed value: ${reason}` : `Could not open Sealed value (context "${context}"): ${reason}`,
|
|
65
|
+
`VAULT_DECRYPT_${reason.toUpperCase()}`,
|
|
66
|
+
options
|
|
67
|
+
);
|
|
68
|
+
this.name = "VaultDecryptError";
|
|
69
|
+
this.reason = reason;
|
|
70
|
+
this.context = context;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/format.ts
|
|
75
|
+
var PREFIX = "vault";
|
|
76
|
+
var FORMAT_VERSION = "v1";
|
|
77
|
+
var KINDS = ["s", "j", "b"];
|
|
78
|
+
function looksSealed(value) {
|
|
79
|
+
return typeof value === "string" && value.startsWith(`${PREFIX}:`);
|
|
80
|
+
}
|
|
81
|
+
function formatSealed(parts) {
|
|
82
|
+
return [
|
|
83
|
+
PREFIX,
|
|
84
|
+
FORMAT_VERSION,
|
|
85
|
+
parts.keyId,
|
|
86
|
+
parts.kind,
|
|
87
|
+
base64urlEncode(parts.iv),
|
|
88
|
+
base64urlEncode(parts.ciphertext)
|
|
89
|
+
].join(":");
|
|
90
|
+
}
|
|
91
|
+
function parseSealed(value, context) {
|
|
92
|
+
if (!looksSealed(value)) throw new VaultDecryptError("not_sealed", context);
|
|
93
|
+
const parts = value.split(":");
|
|
94
|
+
const [, version, keyId, kind, iv, ciphertext] = parts;
|
|
95
|
+
if (parts.length !== 6 || !/^v[0-9]+$/.test(version)) {
|
|
96
|
+
throw new VaultDecryptError("not_sealed", context);
|
|
97
|
+
}
|
|
98
|
+
if (version !== FORMAT_VERSION) {
|
|
99
|
+
throw new VaultDecryptError("unsupported_version", context);
|
|
100
|
+
}
|
|
101
|
+
const ivBytes = base64urlDecode(iv);
|
|
102
|
+
const ciphertextBytes = base64urlDecode(ciphertext);
|
|
103
|
+
if (!/^[0-9a-f]{8}$/.test(keyId) || !isSealedKind(kind) || !ivBytes || !ciphertextBytes) {
|
|
104
|
+
throw new VaultDecryptError("not_sealed", context);
|
|
105
|
+
}
|
|
106
|
+
return { keyId, kind, iv: ivBytes, ciphertext: ciphertextBytes };
|
|
107
|
+
}
|
|
108
|
+
function isSealedKind(kind) {
|
|
109
|
+
return KINDS.includes(kind);
|
|
110
|
+
}
|
|
111
|
+
function additionalData(kind, context) {
|
|
112
|
+
return utf8Encode(`${FORMAT_VERSION}:${kind}:${context}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/keys.ts
|
|
116
|
+
var SALT = utf8Encode("secret-vault/v1");
|
|
117
|
+
var KEY_ID_BYTES = 4;
|
|
118
|
+
async function deriveVaultKey(secret, purpose) {
|
|
119
|
+
const material = await crypto.subtle.importKey(
|
|
120
|
+
"raw",
|
|
121
|
+
utf8Encode(secret),
|
|
122
|
+
"HKDF",
|
|
123
|
+
false,
|
|
124
|
+
["deriveKey", "deriveBits"]
|
|
125
|
+
);
|
|
126
|
+
const hkdf = (info) => ({
|
|
127
|
+
name: "HKDF",
|
|
128
|
+
hash: "SHA-256",
|
|
129
|
+
salt: SALT,
|
|
130
|
+
info: utf8Encode(info)
|
|
131
|
+
});
|
|
132
|
+
const [key, idBits] = await Promise.all([
|
|
133
|
+
crypto.subtle.deriveKey(
|
|
134
|
+
hkdf(purpose),
|
|
135
|
+
material,
|
|
136
|
+
{ name: "AES-GCM", length: 256 },
|
|
137
|
+
false,
|
|
138
|
+
["encrypt", "decrypt"]
|
|
139
|
+
),
|
|
140
|
+
crypto.subtle.deriveBits(
|
|
141
|
+
hkdf(`${purpose}:key-id`),
|
|
142
|
+
material,
|
|
143
|
+
KEY_ID_BYTES * 8
|
|
144
|
+
)
|
|
145
|
+
]);
|
|
146
|
+
const id = Array.from(
|
|
147
|
+
new Uint8Array(idBits),
|
|
148
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
149
|
+
).join("");
|
|
150
|
+
return { id, key };
|
|
151
|
+
}
|
|
152
|
+
var KeyRing = class {
|
|
153
|
+
#secrets;
|
|
154
|
+
#purpose;
|
|
155
|
+
#loaded;
|
|
156
|
+
/**
|
|
157
|
+
* @param secrets - The current Vault secret first, then any previous ones.
|
|
158
|
+
* @param purpose - The Vault's Purpose.
|
|
159
|
+
*/
|
|
160
|
+
constructor(secrets, purpose) {
|
|
161
|
+
this.#secrets = secrets;
|
|
162
|
+
this.#purpose = purpose;
|
|
163
|
+
}
|
|
164
|
+
/** Derives the keys, or returns the cached ones. */
|
|
165
|
+
load() {
|
|
166
|
+
this.#loaded ??= this.#derive().catch((error) => {
|
|
167
|
+
this.#loaded = void 0;
|
|
168
|
+
throw error;
|
|
169
|
+
});
|
|
170
|
+
return this.#loaded;
|
|
171
|
+
}
|
|
172
|
+
async #derive() {
|
|
173
|
+
const keys = await Promise.all(
|
|
174
|
+
this.#secrets.map((secret) => deriveVaultKey(secret, this.#purpose))
|
|
175
|
+
);
|
|
176
|
+
const byId = /* @__PURE__ */ new Map();
|
|
177
|
+
for (const key of keys) {
|
|
178
|
+
if (byId.has(key.id)) {
|
|
179
|
+
throw new VaultConfigError(
|
|
180
|
+
"Vault options `secret` and `previousSecrets` contain two Vault secrets with the same key id; remove the duplicate."
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
byId.set(key.id, key);
|
|
184
|
+
}
|
|
185
|
+
return { current: keys[0], byId };
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// src/random.ts
|
|
190
|
+
function randomBytes(length) {
|
|
191
|
+
return crypto.getRandomValues(new Uint8Array(length));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/vault.ts
|
|
195
|
+
var MIN_SECRET_LENGTH = 32;
|
|
196
|
+
var IV_BYTES = 12;
|
|
197
|
+
var Vault = class {
|
|
198
|
+
#keys;
|
|
199
|
+
/**
|
|
200
|
+
* Checks the options right away; keys are derived on first use, so this
|
|
201
|
+
* needs no `await`.
|
|
202
|
+
*
|
|
203
|
+
* @param options - The Vault secrets and Purpose.
|
|
204
|
+
* @throws {@link VaultConfigError} when an option breaks a rule in
|
|
205
|
+
* {@link VaultOptions}, except a duplicated secret, which is reported on
|
|
206
|
+
* first use. The message never contains a secret.
|
|
207
|
+
*/
|
|
208
|
+
constructor(options) {
|
|
209
|
+
assertSecret(options.secret, "secret");
|
|
210
|
+
const previousSecrets = options.previousSecrets ?? [];
|
|
211
|
+
if (!Array.isArray(previousSecrets)) {
|
|
212
|
+
throw new VaultConfigError(
|
|
213
|
+
"Vault option `previousSecrets` must be an array of strings."
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
previousSecrets.forEach(
|
|
217
|
+
(secret, index) => assertSecret(secret, `previousSecrets[${index}]`)
|
|
218
|
+
);
|
|
219
|
+
if (typeof options.purpose !== "string" || options.purpose === "") {
|
|
220
|
+
throw new VaultConfigError("Vault option `purpose` must not be empty.");
|
|
221
|
+
}
|
|
222
|
+
this.#keys = new KeyRing(
|
|
223
|
+
[options.secret, ...previousSecrets],
|
|
224
|
+
options.purpose
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* True when `value` is a Sealed value: a string starting with `vault:`. Use
|
|
229
|
+
* it to tell Sealed values from plain text still stored in a column.
|
|
230
|
+
*
|
|
231
|
+
* @param value - Any value.
|
|
232
|
+
*/
|
|
233
|
+
static isSealed(value) {
|
|
234
|
+
return looksSealed(value);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Seals a string.
|
|
238
|
+
*
|
|
239
|
+
* @param plaintext - The string to seal. May be empty.
|
|
240
|
+
* @param context - Where the Sealed value is stored.
|
|
241
|
+
* @returns The Sealed value, `vault:v1:<keyId>:s:<iv>:<ciphertext>`.
|
|
242
|
+
*/
|
|
243
|
+
encrypt(plaintext, context) {
|
|
244
|
+
return this.#seal("s", utf8Encode(plaintext), context);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Opens a Sealed value made by {@link Vault.encrypt}.
|
|
248
|
+
*
|
|
249
|
+
* @param sealed - The Sealed value.
|
|
250
|
+
* @param context - The Context it was sealed under.
|
|
251
|
+
* @returns The Plaintext string.
|
|
252
|
+
* @throws {@link VaultDecryptError} when the value isn't a Sealed value, was
|
|
253
|
+
* sealed with an unknown Vault secret or another Purpose, was edited, or is
|
|
254
|
+
* opened under another Context, or `wrong_kind` when it isn't a string
|
|
255
|
+
* Sealed value. A string Sealed value that opens to bytes that aren't valid
|
|
256
|
+
* UTF-8 also rejects as `authentication_failed`, with the decoder's
|
|
257
|
+
* `TypeError` as its `cause`.
|
|
258
|
+
*/
|
|
259
|
+
async decrypt(sealed, context) {
|
|
260
|
+
const plaintext = await this.#openKind("s", sealed, context);
|
|
261
|
+
try {
|
|
262
|
+
return utf8Decode(plaintext);
|
|
263
|
+
} catch (cause) {
|
|
264
|
+
throw new VaultDecryptError("authentication_failed", context, { cause });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Seals JSON data with plain `JSON.stringify`.
|
|
269
|
+
*
|
|
270
|
+
* @param value - Any value `JSON.stringify` can encode.
|
|
271
|
+
* @param context - Where the Sealed value is stored.
|
|
272
|
+
* @returns The Sealed value, `vault:v1:<keyId>:j:<iv>:<ciphertext>`.
|
|
273
|
+
* @throws The promise rejects with a `TypeError` when `JSON.stringify`
|
|
274
|
+
* can't encode `value`, such as `undefined`, a function, a `BigInt` or a
|
|
275
|
+
* cyclic object, before anything is encrypted. It never throws
|
|
276
|
+
* synchronously.
|
|
277
|
+
*/
|
|
278
|
+
async encryptJson(value, context) {
|
|
279
|
+
const json = JSON.stringify(value);
|
|
280
|
+
if (typeof json !== "string") {
|
|
281
|
+
throw new TypeError("Vault.encryptJson: value can't be encoded as JSON.");
|
|
282
|
+
}
|
|
283
|
+
return this.#seal("j", utf8Encode(json), context);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Opens a Sealed value made by {@link Vault.encryptJson}.
|
|
287
|
+
*
|
|
288
|
+
* @typeParam T - The type the caller expects. It is asserted, not validated.
|
|
289
|
+
* @param sealed - The Sealed value.
|
|
290
|
+
* @param context - The Context it was sealed under.
|
|
291
|
+
* @returns The JSON data, parsed with plain `JSON.parse`.
|
|
292
|
+
* @throws {@link VaultDecryptError} for the same reasons as
|
|
293
|
+
* {@link Vault.decrypt}, `wrong_kind` when it isn't a JSON Sealed value, or
|
|
294
|
+
* `invalid_json` when it opens to text that isn't JSON.
|
|
295
|
+
*/
|
|
296
|
+
async decryptJson(sealed, context) {
|
|
297
|
+
const plaintext = await this.#openKind("j", sealed, context);
|
|
298
|
+
try {
|
|
299
|
+
return JSON.parse(utf8Decode(plaintext));
|
|
300
|
+
} catch (cause) {
|
|
301
|
+
throw new VaultDecryptError("invalid_json", context, { cause });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Seals bytes.
|
|
306
|
+
*
|
|
307
|
+
* @param plaintext - The bytes to seal. May be empty.
|
|
308
|
+
* @param context - Where the Sealed value is stored.
|
|
309
|
+
* @returns The Sealed value, `vault:v1:<keyId>:b:<iv>:<ciphertext>`.
|
|
310
|
+
*/
|
|
311
|
+
encryptBytes(plaintext, context) {
|
|
312
|
+
return this.#seal("b", Uint8Array.from(plaintext), context);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Opens a Sealed value made by {@link Vault.encryptBytes}.
|
|
316
|
+
*
|
|
317
|
+
* @param sealed - The Sealed value.
|
|
318
|
+
* @param context - The Context it was sealed under.
|
|
319
|
+
* @returns The Plaintext bytes, as a plain `Uint8Array`.
|
|
320
|
+
* @throws {@link VaultDecryptError} for the same reasons as
|
|
321
|
+
* {@link Vault.decrypt}, or `wrong_kind` when it isn't a bytes Sealed value.
|
|
322
|
+
*/
|
|
323
|
+
decryptBytes(sealed, context) {
|
|
324
|
+
return this.#openKind("b", sealed, context);
|
|
325
|
+
}
|
|
326
|
+
/** Opens a Sealed value, then checks it holds the `expected` kind. */
|
|
327
|
+
async #openKind(expected, sealed, context) {
|
|
328
|
+
const { kind, plaintext } = await this.#open(sealed, context);
|
|
329
|
+
if (kind !== expected) throw new VaultDecryptError("wrong_kind", context);
|
|
330
|
+
return plaintext;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* True when a Sealed value was made with a previous Vault secret, so it
|
|
334
|
+
* should be re-sealed with {@link Vault.reseal} before that secret is
|
|
335
|
+
* dropped. Reads only the key id; it doesn't open the value, so it needs no
|
|
336
|
+
* Context. It is async, unlike {@link Vault.isSealed}, because the current
|
|
337
|
+
* key id is derived on first use: `await` it.
|
|
338
|
+
*
|
|
339
|
+
* @param sealed - The Sealed value.
|
|
340
|
+
* @returns A promise of `true` when the key id isn't the current one.
|
|
341
|
+
* @throws The promise rejects with {@link VaultDecryptError} `not_sealed`
|
|
342
|
+
* (or `unsupported_version`) when the value isn't a Sealed value this
|
|
343
|
+
* package can read; its `context` is `undefined`. It never throws
|
|
344
|
+
* synchronously.
|
|
345
|
+
*/
|
|
346
|
+
async needsRotation(sealed) {
|
|
347
|
+
const { keyId } = parseSealed(sealed, void 0);
|
|
348
|
+
const { current } = await this.#keys.load();
|
|
349
|
+
return keyId !== current.id;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Opens a Sealed value with whichever Vault secret made it and seals it
|
|
353
|
+
* again with the current one, keeping its kind (string, JSON or bytes). Use
|
|
354
|
+
* it on values where {@link Vault.needsRotation} is true.
|
|
355
|
+
*
|
|
356
|
+
* @param sealed - The Sealed value.
|
|
357
|
+
* @param context - The Context it was sealed under; the new Sealed value
|
|
358
|
+
* is bound to the same Context.
|
|
359
|
+
* @returns A new Sealed value with the current key id.
|
|
360
|
+
* @throws {@link VaultDecryptError} for the same reasons as
|
|
361
|
+
* {@link Vault.decrypt}.
|
|
362
|
+
*/
|
|
363
|
+
async reseal(sealed, context) {
|
|
364
|
+
const { kind, plaintext } = await this.#open(sealed, context);
|
|
365
|
+
return this.#seal(kind, plaintext, context);
|
|
366
|
+
}
|
|
367
|
+
async #seal(kind, plaintext, context) {
|
|
368
|
+
const { current } = await this.#keys.load();
|
|
369
|
+
const iv = randomBytes(IV_BYTES);
|
|
370
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
371
|
+
aesGcmParams(iv, kind, context),
|
|
372
|
+
current.key,
|
|
373
|
+
plaintext
|
|
374
|
+
);
|
|
375
|
+
return formatSealed({
|
|
376
|
+
keyId: current.id,
|
|
377
|
+
kind,
|
|
378
|
+
iv,
|
|
379
|
+
ciphertext: new Uint8Array(ciphertext)
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async #open(sealed, context) {
|
|
383
|
+
const { keyId, kind, iv, ciphertext } = parseSealed(sealed, context);
|
|
384
|
+
const { byId } = await this.#keys.load();
|
|
385
|
+
const vaultKey = byId.get(keyId);
|
|
386
|
+
if (!vaultKey) throw new VaultDecryptError("unknown_key", context);
|
|
387
|
+
try {
|
|
388
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
389
|
+
aesGcmParams(iv, kind, context),
|
|
390
|
+
vaultKey.key,
|
|
391
|
+
ciphertext
|
|
392
|
+
);
|
|
393
|
+
return { kind, plaintext: new Uint8Array(plaintext) };
|
|
394
|
+
} catch (cause) {
|
|
395
|
+
throw new VaultDecryptError("authentication_failed", context, { cause });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
function aesGcmParams(iv, kind, context) {
|
|
400
|
+
return { name: "AES-GCM", iv, additionalData: additionalData(kind, context) };
|
|
401
|
+
}
|
|
402
|
+
function assertSecret(secret, option) {
|
|
403
|
+
if (typeof secret !== "string" || secret.length < MIN_SECRET_LENGTH) {
|
|
404
|
+
throw new VaultConfigError(
|
|
405
|
+
`Vault option \`${option}\` must be a string of at least ${MIN_SECRET_LENGTH} characters.`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export { Vault, VaultConfigError, VaultDecryptError, VaultError };
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rezwanul7/secret-vault",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/rezwanul7/turbopack-package-development.git",
|
|
8
|
+
"directory": "packages/secret-vault"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.cjs",
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@edge-runtime/vm": "^5.0.0",
|
|
38
|
+
"@repo/eslint-config": "0.0.0",
|
|
39
|
+
"@repo/typescript-config": "0.0.0",
|
|
40
|
+
"@vitest/coverage-v8": "4.0.18",
|
|
41
|
+
"eslint": "^9.39.1",
|
|
42
|
+
"tsup": "^8.5.1",
|
|
43
|
+
"typedoc-plugin-markdown": "4.14.0",
|
|
44
|
+
"vitest": "^4.0.18"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"dev": "tsup --watch",
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"check-types": "tsc --noEmit",
|
|
50
|
+
"check-docs": "typedoc --readme none --disableSources",
|
|
51
|
+
"check-package": "publint && attw --pack . --profile node16",
|
|
52
|
+
"lint": "eslint . --max-warnings 0",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:watch": "vitest",
|
|
55
|
+
"test:coverage": "vitest run --coverage"
|
|
56
|
+
}
|
|
6
57
|
}
|