@xdevplatform/chat-xdk 0.1.0 → 0.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/LICENSE +21 -0
- package/README.md +27 -23
- package/index.d.ts +365 -77
- package/index.js +178 -47
- package/package.json +6 -2
- package/pkg/chat_xdk_wasm.d.ts +124 -44
- package/pkg/chat_xdk_wasm.js +224 -223
- package/pkg/chat_xdk_wasm_bg.wasm +0 -0
- package/pkg/chat_xdk_wasm_bg.wasm.d.ts +15 -9
- package/pkg/package.json +21 -0
package/index.js
CHANGED
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
* The Rust WASM `Chat` is a pure crypto engine that never touches
|
|
6
6
|
* globalThis or module-level singletons.
|
|
7
7
|
*
|
|
8
|
-
* Each `ChatWithJuicebox` instance holds its
|
|
9
|
-
*
|
|
8
|
+
* Each `ChatWithJuicebox` instance holds its own Juicebox client, but the
|
|
9
|
+
* juicebox-sdk WASM resolves its auth-token callback through a single
|
|
10
|
+
* process-global (`JuiceboxGetAuthToken`). Every Juicebox operation re-arms
|
|
11
|
+
* that global with its own instance's token getter just before running, so
|
|
12
|
+
* *sequential* use of multiple instances in one runtime is correct.
|
|
13
|
+
* *Concurrent* Juicebox operations across instances in one process are not
|
|
14
|
+
* supported: the last-armed instance's token getter services all in-flight
|
|
15
|
+
* calls.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
function isNodeRuntime() {
|
|
@@ -46,11 +52,13 @@ function toPinBytes(pin) {
|
|
|
46
52
|
* descending run of digits. Never applied on unlock.
|
|
47
53
|
*/
|
|
48
54
|
function validatePinStrength(pinBytes) {
|
|
55
|
+
// Error messages match the native core's weak-PIN errors byte-for-byte so
|
|
56
|
+
// callers can match on them across bindings.
|
|
49
57
|
if (pinBytes.length < MIN_PIN_LENGTH) {
|
|
50
|
-
throw new Error(`
|
|
58
|
+
throw new Error(`PIN must be at least ${MIN_PIN_LENGTH} characters`);
|
|
51
59
|
}
|
|
52
60
|
if (pinBytes.every((b) => b === pinBytes[0])) {
|
|
53
|
-
throw new Error('
|
|
61
|
+
throw new Error('PIN must not be a single repeated character');
|
|
54
62
|
}
|
|
55
63
|
const allDigits = pinBytes.every((b) => b >= 0x30 && b <= 0x39);
|
|
56
64
|
let ascending = true;
|
|
@@ -60,7 +68,7 @@ function validatePinStrength(pinBytes) {
|
|
|
60
68
|
if (pinBytes[i] !== pinBytes[i - 1] - 1) descending = false;
|
|
61
69
|
}
|
|
62
70
|
if (allDigits && (ascending || descending)) {
|
|
63
|
-
throw new Error('
|
|
71
|
+
throw new Error('PIN must not be a sequential run of digits');
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
|
|
@@ -90,9 +98,15 @@ async function initWasmModule() {
|
|
|
90
98
|
let wasmModule;
|
|
91
99
|
let init;
|
|
92
100
|
if (isNodeRuntime()) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
// Node 18 ships WebCrypto but does not expose it on the global scope,
|
|
102
|
+
// and the wasm module's random-byte source requires globalThis.crypto.
|
|
103
|
+
if (typeof globalThis.crypto === "undefined") {
|
|
104
|
+
const { webcrypto } = await import("crypto");
|
|
105
|
+
globalThis.crypto = webcrypto;
|
|
106
|
+
}
|
|
107
|
+
// Dynamic import, not require(): the wasm glue is ESM, and require(esm)
|
|
108
|
+
// only exists on Node >= 20.19 while this package supports Node 18+.
|
|
109
|
+
wasmModule = await import("./pkg/chat_xdk_wasm.js");
|
|
96
110
|
init =
|
|
97
111
|
wasmModule?.default ||
|
|
98
112
|
wasmModule?.init ||
|
|
@@ -141,6 +155,74 @@ async function loadJuiceboxSdk() {
|
|
|
141
155
|
return await import("juicebox-sdk");
|
|
142
156
|
}
|
|
143
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Resolve the PIN guess budget for Juicebox registration, matching the
|
|
160
|
+
* native bindings: a `max_guess_count` that is a non-negative integer
|
|
161
|
+
* (including 0) applies as-is; a fractional, negative, or non-numeric value
|
|
162
|
+
* falls back to the shape default — 20 for the `sdk_config` shape, 5
|
|
163
|
+
* otherwise. An explicit `maxGuessCount` option overrides the config under
|
|
164
|
+
* the same integer rule. Exported for tests; `createChat` calls it
|
|
165
|
+
* internally.
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} configValue - Parsed Juicebox config JSON from the X API
|
|
168
|
+
* @param {number} [maxGuessCount] - Explicit override from createChat options
|
|
169
|
+
* @returns {number} The number of PIN guesses to register with
|
|
170
|
+
*/
|
|
171
|
+
export function resolveMaxGuessCount(configValue, maxGuessCount) {
|
|
172
|
+
if (Number.isSafeInteger(maxGuessCount) && maxGuessCount >= 0) {
|
|
173
|
+
return maxGuessCount;
|
|
174
|
+
}
|
|
175
|
+
const fromConfig = configValue?.max_guess_count;
|
|
176
|
+
if (Number.isSafeInteger(fromConfig) && fromConfig >= 0) {
|
|
177
|
+
return fromConfig;
|
|
178
|
+
}
|
|
179
|
+
return typeof configValue?.sdk_config === "string" ? 20 : 5;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Derive the value to hand the Juicebox `Configuration` constructor from a
|
|
184
|
+
* parsed X API config. The `sdk_config` wrapper shape is unwrapped to its
|
|
185
|
+
* embedded SDK config JSON string (which the Juicebox constructor accepts
|
|
186
|
+
* directly); a raw realms config passes through unchanged; the `token_map`
|
|
187
|
+
* shape (an array of `{ key, value: { address } }` entries) is converted to
|
|
188
|
+
* a realms config with majority recover threshold, the same derivation the
|
|
189
|
+
* native bindings apply. A non-array `token_map` is rejected here, with the
|
|
190
|
+
* same message the core parser uses, rather than handed to the Juicebox
|
|
191
|
+
* constructor to fail obscurely. Realm auth tokens are not taken from the
|
|
192
|
+
* config in this wrapper — they come from the `getAuthToken` callback.
|
|
193
|
+
*/
|
|
194
|
+
export function juiceboxClientConfig(configValue) {
|
|
195
|
+
if (typeof configValue?.sdk_config === "string") {
|
|
196
|
+
return configValue.sdk_config;
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
configValue?.token_map !== undefined &&
|
|
200
|
+
configValue?.realms === undefined &&
|
|
201
|
+
!Array.isArray(configValue.token_map)
|
|
202
|
+
) {
|
|
203
|
+
throw new Error("Missing token_map or sdk_config");
|
|
204
|
+
}
|
|
205
|
+
if (Array.isArray(configValue?.token_map) && configValue?.realms === undefined) {
|
|
206
|
+
const realms = configValue.token_map.map((entry) => {
|
|
207
|
+
const id = entry?.key;
|
|
208
|
+
const address = entry?.value?.address;
|
|
209
|
+
if (typeof id !== "string" || typeof address !== "string") {
|
|
210
|
+
throw new Error(
|
|
211
|
+
"Invalid token_map entry: expected { key, value: { address } }",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return { id, address };
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
realms,
|
|
218
|
+
register_threshold: realms.length,
|
|
219
|
+
recover_threshold: Math.floor(realms.length / 2) + 1,
|
|
220
|
+
pin_hashing_mode: "Standard2019",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return configValue;
|
|
224
|
+
}
|
|
225
|
+
|
|
144
226
|
// ChatWithJuicebox — JS wrapper that owns both a WasmChat and a Juicebox client
|
|
145
227
|
|
|
146
228
|
/**
|
|
@@ -153,18 +235,36 @@ export class ChatWithJuicebox {
|
|
|
153
235
|
#inner;
|
|
154
236
|
#juiceboxClient;
|
|
155
237
|
#numGuesses;
|
|
238
|
+
// The raw `maxGuessCount` createChat option (undefined when not provided),
|
|
239
|
+
// kept so updateConfig re-resolves the budget under the same override
|
|
240
|
+
// semantics as construction: an explicit option keeps winning.
|
|
241
|
+
#maxGuessCountOverride;
|
|
156
242
|
#JuiceboxConfiguration;
|
|
157
243
|
#JuiceboxClient;
|
|
244
|
+
// Points the process-global `JuiceboxGetAuthToken` at this instance's token
|
|
245
|
+
// getter. Called before every Juicebox network operation so sequential use
|
|
246
|
+
// of multiple instances presents the right tokens (last-armed wins).
|
|
247
|
+
#armAuthTokenHook;
|
|
158
248
|
|
|
159
|
-
constructor(
|
|
249
|
+
constructor(
|
|
250
|
+
wasmChat,
|
|
251
|
+
juiceboxClient,
|
|
252
|
+
numGuesses,
|
|
253
|
+
JuiceboxConfiguration,
|
|
254
|
+
JuiceboxClientCtor,
|
|
255
|
+
armAuthTokenHook,
|
|
256
|
+
maxGuessCountOverride,
|
|
257
|
+
) {
|
|
160
258
|
this.#inner = wasmChat;
|
|
161
259
|
this.#juiceboxClient = juiceboxClient;
|
|
162
260
|
this.#numGuesses = numGuesses;
|
|
261
|
+
this.#maxGuessCountOverride = maxGuessCountOverride;
|
|
163
262
|
this.#JuiceboxConfiguration = JuiceboxConfiguration;
|
|
164
263
|
this.#JuiceboxClient = JuiceboxClientCtor;
|
|
264
|
+
this.#armAuthTokenHook = armAuthTokenHook ?? (() => {});
|
|
165
265
|
}
|
|
166
266
|
|
|
167
|
-
// Juicebox lifecycle (
|
|
267
|
+
// Juicebox lifecycle (handled in this JS layer)
|
|
168
268
|
|
|
169
269
|
/**
|
|
170
270
|
* Register existing keys with Juicebox. Call generateKeypairs() first.
|
|
@@ -178,6 +278,7 @@ export class ChatWithJuicebox {
|
|
|
178
278
|
validatePinStrength(pinBytes);
|
|
179
279
|
const secretBytes = this.#inner.exportKeys();
|
|
180
280
|
try {
|
|
281
|
+
this.#armAuthTokenHook();
|
|
181
282
|
await this.#juiceboxClient.register(
|
|
182
283
|
pinBytes,
|
|
183
284
|
secretBytes,
|
|
@@ -202,6 +303,7 @@ export class ChatWithJuicebox {
|
|
|
202
303
|
const ownsPin = typeof pin === 'string';
|
|
203
304
|
let secretBytes;
|
|
204
305
|
try {
|
|
306
|
+
this.#armAuthTokenHook();
|
|
205
307
|
secretBytes = await this.#juiceboxClient.recover(pinBytes, new Uint8Array(0));
|
|
206
308
|
} catch (err) {
|
|
207
309
|
throw new Error(`Juicebox recovery failed: ${formatJuiceboxError(err, RECOVER_REASON_BY_VALUE)}`);
|
|
@@ -217,6 +319,7 @@ export class ChatWithJuicebox {
|
|
|
217
319
|
|
|
218
320
|
/** Delete keys from Juicebox. Warning: Irreversible. */
|
|
219
321
|
async delete() {
|
|
322
|
+
this.#armAuthTokenHook();
|
|
220
323
|
await this.#juiceboxClient.delete();
|
|
221
324
|
this.#inner.lock();
|
|
222
325
|
}
|
|
@@ -229,6 +332,7 @@ export class ChatWithJuicebox {
|
|
|
229
332
|
await this.unlock(oldPin);
|
|
230
333
|
const secretBytes = this.#inner.exportKeys();
|
|
231
334
|
try {
|
|
335
|
+
this.#armAuthTokenHook();
|
|
232
336
|
await this.#juiceboxClient.register(
|
|
233
337
|
newPinBytes,
|
|
234
338
|
secretBytes,
|
|
@@ -243,11 +347,16 @@ export class ChatWithJuicebox {
|
|
|
243
347
|
}
|
|
244
348
|
}
|
|
245
349
|
|
|
246
|
-
/**
|
|
350
|
+
/**
|
|
351
|
+
* Update Juicebox config (e.g. to refresh auth tokens). Re-creates the
|
|
352
|
+
* client and re-resolves the PIN guess budget from the new config; an
|
|
353
|
+
* explicit createChat `maxGuessCount` override keeps winning.
|
|
354
|
+
*/
|
|
247
355
|
updateConfig(juiceboxConfig) {
|
|
248
356
|
const configValue = JSON.parse(juiceboxConfig);
|
|
249
|
-
const config = new this.#JuiceboxConfiguration(configValue);
|
|
357
|
+
const config = new this.#JuiceboxConfiguration(juiceboxClientConfig(configValue));
|
|
250
358
|
this.#juiceboxClient = new this.#JuiceboxClient(config, []);
|
|
359
|
+
this.#numGuesses = resolveMaxGuessCount(configValue, this.#maxGuessCountOverride);
|
|
251
360
|
}
|
|
252
361
|
|
|
253
362
|
// Forwarded crypto methods — delegate to inner WasmChat
|
|
@@ -261,35 +370,42 @@ export class ChatWithJuicebox {
|
|
|
261
370
|
hasIdentityKey() { return this.#inner.hasIdentityKey(); }
|
|
262
371
|
lock() { return this.#inner.lock(); }
|
|
263
372
|
decryptConversationKey(b64) { return this.#inner.decryptConversationKey(b64); }
|
|
264
|
-
generateConversationKey() { return this.#inner.generateConversationKey(); }
|
|
265
373
|
extractConversationKeys(events) { return this.#inner.extractConversationKeys(events); }
|
|
266
374
|
decryptEvent(eventB64, conversationKeys, signingKeys) {
|
|
267
|
-
return this.#inner.decryptEvent(eventB64, conversationKeys, signingKeys
|
|
375
|
+
return this.#inner.decryptEvent(eventB64, conversationKeys, signingKeys);
|
|
268
376
|
}
|
|
269
377
|
decryptEvents(events, signingKeys) {
|
|
270
|
-
return this.#inner.decryptEvents(events, signingKeys
|
|
378
|
+
return this.#inner.decryptEvents(events, signingKeys);
|
|
271
379
|
}
|
|
272
380
|
sign(data) { return this.#inner.sign(data); }
|
|
273
381
|
verify(publicKeyB64, signature, data) { return this.#inner.verify(publicKeyB64, signature, data); }
|
|
274
382
|
verifyKeyBinding(identityPublicKeyB64, signingPublicKeyB64, identityPublicKeySignatureB64) {
|
|
275
383
|
return this.#inner.verifyKeyBinding(identityPublicKeyB64, signingPublicKeyB64, identityPublicKeySignatureB64);
|
|
276
384
|
}
|
|
277
|
-
encryptMessage(
|
|
278
|
-
encryptReply(
|
|
279
|
-
encryptAddReaction(
|
|
280
|
-
encryptRemoveReaction(
|
|
385
|
+
encryptMessage(params) { return this.#inner.encryptMessage(params); }
|
|
386
|
+
encryptReply(params) { return this.#inner.encryptReply(params); }
|
|
387
|
+
encryptAddReaction(params) { return this.#inner.encryptAddReaction(params); }
|
|
388
|
+
encryptRemoveReaction(params) { return this.#inner.encryptRemoveReaction(params); }
|
|
281
389
|
encryptStream(plaintext, key) { return this.#inner.encryptStream(plaintext, key); }
|
|
282
390
|
decryptStream(encrypted, key) { return this.#inner.decryptStream(encrypted, key); }
|
|
391
|
+
streamEncryptor(key) { return this.#inner.streamEncryptor(key); }
|
|
392
|
+
streamDecryptor(key) { return this.#inner.streamDecryptor(key); }
|
|
283
393
|
encrypt(plaintext, key) { return this.#inner.encrypt(plaintext, key); }
|
|
284
394
|
decrypt(ciphertext, key) { return this.#inner.decrypt(ciphertext, key); }
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
395
|
+
prepareConversationKeyChange(params) { return this.#inner.prepareConversationKeyChange(params); }
|
|
396
|
+
prepareGroupMembersChange(params) { return this.#inner.prepareGroupMembersChange(params); }
|
|
397
|
+
prepareGroupCreate(params) { return this.#inner.prepareGroupCreate(params); }
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Release the WASM-side crypto engine: clears key material (`lock()`) and
|
|
401
|
+
* frees the underlying WASM object. The instance must not be used
|
|
402
|
+
* afterwards. When reusing the instance, `lock()` alone suffices for key
|
|
403
|
+
* hygiene.
|
|
404
|
+
*/
|
|
405
|
+
free() {
|
|
406
|
+
this.#inner.lock();
|
|
407
|
+
this.#inner.free();
|
|
290
408
|
}
|
|
291
|
-
signAddMembers(...args) { return this.#inner.signAddMembers(...args); }
|
|
292
|
-
signKeyChange(...args) { return this.#inner.signKeyChange(...args); }
|
|
293
409
|
}
|
|
294
410
|
|
|
295
411
|
/**
|
|
@@ -299,7 +415,9 @@ export class ChatWithJuicebox {
|
|
|
299
415
|
* @param {string} options.juiceboxConfig - Juicebox configuration JSON from X API
|
|
300
416
|
* @param {Function} options.getAuthToken - Async function to get auth token for a realm
|
|
301
417
|
* Signature: (realmId: string) => Promise<string>
|
|
302
|
-
* @
|
|
418
|
+
* @param {number} [options.maxGuessCount] - Optional override for the PIN guess
|
|
419
|
+
* budget (see CreateChatOptions)
|
|
420
|
+
* @returns {Promise<ChatWithJuicebox>} Initialized chat instance
|
|
303
421
|
*
|
|
304
422
|
* @example
|
|
305
423
|
* const chat = await createChat({
|
|
@@ -328,21 +446,29 @@ export async function createChat(options) {
|
|
|
328
446
|
throw new Error('getAuthToken must be an async function');
|
|
329
447
|
}
|
|
330
448
|
|
|
331
|
-
// Load Juicebox WASM SDK
|
|
449
|
+
// Load Juicebox WASM SDK. Tests inject stub constructors through the
|
|
450
|
+
// internal `juiceboxModule` option so this flow runs without the
|
|
451
|
+
// juicebox-sdk WASM or network realms; it is not part of the public API.
|
|
332
452
|
let JuiceboxClientCtor;
|
|
333
453
|
let JuiceboxConfiguration;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
454
|
+
if (options.juiceboxModule) {
|
|
455
|
+
JuiceboxClientCtor = options.juiceboxModule.Client;
|
|
456
|
+
JuiceboxConfiguration = options.juiceboxModule.Configuration;
|
|
457
|
+
} else {
|
|
458
|
+
try {
|
|
459
|
+
const juiceboxModule = await loadJuiceboxSdk();
|
|
460
|
+
JuiceboxClientCtor = juiceboxModule.Client;
|
|
461
|
+
JuiceboxConfiguration = juiceboxModule.Configuration;
|
|
462
|
+
} catch (e) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
"Failed to load juicebox-sdk (an optional peer dependency). " +
|
|
465
|
+
"Install it with `npm install juicebox-sdk`, or build it from " +
|
|
466
|
+
"https://github.com/juicebox-systems/juicebox-sdk:\n" +
|
|
467
|
+
" cd juicebox-sdk/rust/sdk/bridge/wasm && wasm-pack build --target nodejs --out-dir pkg --out-name juicebox-sdk\n" +
|
|
468
|
+
" npm install /path/to/juicebox-sdk/rust/sdk/bridge/wasm/pkg\n" +
|
|
469
|
+
"Original error: " + e.message
|
|
470
|
+
);
|
|
471
|
+
}
|
|
346
472
|
}
|
|
347
473
|
if (!JuiceboxConfiguration) {
|
|
348
474
|
throw new Error('juicebox-sdk Configuration export is missing');
|
|
@@ -350,8 +476,10 @@ export async function createChat(options) {
|
|
|
350
476
|
|
|
351
477
|
// The juicebox-sdk WASM resolves `JuiceboxGetAuthToken` as a bare-name
|
|
352
478
|
// global on every register/recover/delete call (not just construction).
|
|
353
|
-
//
|
|
354
|
-
|
|
479
|
+
// The global is process-wide, so each instance re-arms it with its own
|
|
480
|
+
// token getter immediately before every Juicebox operation; concurrent
|
|
481
|
+
// Juicebox operations across instances are not supported (last-armed wins).
|
|
482
|
+
const authTokenHook = async (realmId) => {
|
|
355
483
|
const realmIdHex =
|
|
356
484
|
typeof realmId === "string"
|
|
357
485
|
? realmId
|
|
@@ -360,15 +488,16 @@ export async function createChat(options) {
|
|
|
360
488
|
.join("");
|
|
361
489
|
return await getAuthToken(realmIdHex);
|
|
362
490
|
};
|
|
491
|
+
const armAuthTokenHook = () => {
|
|
492
|
+
globalThis.JuiceboxGetAuthToken = authTokenHook;
|
|
493
|
+
};
|
|
494
|
+
armAuthTokenHook();
|
|
363
495
|
|
|
364
496
|
const configValue = JSON.parse(juiceboxConfig);
|
|
365
|
-
const config = new JuiceboxConfiguration(configValue);
|
|
497
|
+
const config = new JuiceboxConfiguration(juiceboxClientConfig(configValue));
|
|
366
498
|
const juiceboxClient = new JuiceboxClientCtor(config, []);
|
|
367
499
|
|
|
368
|
-
const numGuesses =
|
|
369
|
-
Number.isFinite(maxGuessCount) && Number(maxGuessCount) > 0
|
|
370
|
-
? Math.floor(Number(maxGuessCount))
|
|
371
|
-
: 5;
|
|
500
|
+
const numGuesses = resolveMaxGuessCount(configValue, maxGuessCount);
|
|
372
501
|
|
|
373
502
|
// Load Rust WASM crypto engine
|
|
374
503
|
const wasmModule = await initWasmModule();
|
|
@@ -380,6 +509,8 @@ export async function createChat(options) {
|
|
|
380
509
|
numGuesses,
|
|
381
510
|
JuiceboxConfiguration,
|
|
382
511
|
JuiceboxClientCtor,
|
|
512
|
+
armAuthTokenHook,
|
|
513
|
+
maxGuessCount,
|
|
383
514
|
);
|
|
384
515
|
}
|
|
385
516
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xdevplatform/chat-xdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "X Chat SDK - End-to-end encryption for X Direct Messages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./index.js"
|
|
10
10
|
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
11
14
|
"files": [
|
|
12
15
|
"index.js",
|
|
13
16
|
"index.d.ts",
|
|
14
|
-
"pkg
|
|
17
|
+
"pkg",
|
|
18
|
+
"LICENSE"
|
|
15
19
|
],
|
|
16
20
|
"publishConfig": {
|
|
17
21
|
"access": "public"
|