@candledottv/cli 0.8.0 → 0.8.1

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 (2) hide show
  1. package/dist/index.js +181 -52
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -18761,6 +18761,24 @@ function isLoopbackHost2(hostname) {
18761
18761
  return true;
18762
18762
  return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
18763
18763
  }
18764
+ function isPrivateHost2(hostname) {
18765
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
18766
+ if (!host.includes(".") && !host.includes(":"))
18767
+ return true;
18768
+ if (/\.(local|internal|home\.arpa)$/.test(host))
18769
+ return true;
18770
+ if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host))
18771
+ return true;
18772
+ if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(host))
18773
+ return true;
18774
+ if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(host))
18775
+ return true;
18776
+ if (/^169\.254\.\d{1,3}\.\d{1,3}$/.test(host))
18777
+ return true;
18778
+ if (/^f[cd][0-9a-f]{2}:/.test(host))
18779
+ return true;
18780
+ return /^fe[89ab][0-9a-f]:/.test(host);
18781
+ }
18764
18782
  function assertTransportSecurity(apiUrl, env) {
18765
18783
  let parsed;
18766
18784
  try {
@@ -18775,9 +18793,9 @@ function assertTransportSecurity(apiUrl, env) {
18775
18793
  }
18776
18794
  if (isLoopbackHost2(parsed.hostname))
18777
18795
  return;
18778
- if (env.CANDLE_ALLOW_INSECURE_HTTP?.trim())
18796
+ if (env.CANDLE_ALLOW_INSECURE_HTTP?.trim() && isPrivateHost2(parsed.hostname))
18779
18797
  return;
18780
- throw new Error(`Refusing to send credentials in the clear to ${parsed.origin}. Set CANDLE_API_URL to an https:// ` + "URL, or set CANDLE_ALLOW_INSECURE_HTTP=1 if this really is a trusted local endpoint.");
18798
+ throw new Error(`Refusing to send credentials in the clear to ${parsed.origin}. Set CANDLE_API_URL to an https:// ` + (isPrivateHost2(parsed.hostname) ? "URL, or set CANDLE_ALLOW_INSECURE_HTTP=1 if this really is a trusted local endpoint." : "URL. CANDLE_ALLOW_INSECURE_HTTP does not apply here: it covers private networks only, " + "and this is a public address."));
18781
18799
  }
18782
18800
  function resolveConfig(env = process.env) {
18783
18801
  const apiUrl = env.CANDLE_API_URL?.trim() || DEFAULT_API_URL2;
@@ -19527,7 +19545,7 @@ var init_tools = __esm(() => {
19527
19545
  });
19528
19546
 
19529
19547
  // ../mcp/src/version.ts
19530
- var SERVER_VERSION = "0.5.0";
19548
+ var SERVER_VERSION = "0.6.2";
19531
19549
 
19532
19550
  // ../mcp/src/server.ts
19533
19551
  var exports_server = {};
@@ -19578,6 +19596,24 @@ function isLoopbackHost(hostname) {
19578
19596
  return true;
19579
19597
  return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
19580
19598
  }
19599
+ function isPrivateHost(hostname) {
19600
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
19601
+ if (!host.includes(".") && !host.includes(":"))
19602
+ return true;
19603
+ if (/\.(local|internal|home\.arpa)$/.test(host))
19604
+ return true;
19605
+ if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host))
19606
+ return true;
19607
+ if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(host))
19608
+ return true;
19609
+ if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(host))
19610
+ return true;
19611
+ if (/^169\.254\.\d{1,3}\.\d{1,3}$/.test(host))
19612
+ return true;
19613
+ if (/^f[cd][0-9a-f]{2}:/.test(host))
19614
+ return true;
19615
+ return /^fe[89ab][0-9a-f]:/.test(host);
19616
+ }
19581
19617
  function insecureApiUrlFault(url, env = process.env) {
19582
19618
  let parsed;
19583
19619
  try {
@@ -19589,9 +19625,9 @@ function insecureApiUrlFault(url, env = process.env) {
19589
19625
  return;
19590
19626
  if (isLoopbackHost(parsed.hostname))
19591
19627
  return;
19592
- if (env[ALLOW_INSECURE_HTTP_ENV]?.trim())
19628
+ if (env[ALLOW_INSECURE_HTTP_ENV]?.trim() && isPrivateHost(parsed.hostname))
19593
19629
  return;
19594
- return `Refusing to send credentials in the clear to ${parsed.origin}. Use https://, or set ` + `${ALLOW_INSECURE_HTTP_ENV}=1 if this really is a trusted local endpoint.`;
19630
+ return `Refusing to send credentials in the clear to ${parsed.origin}. Use https://` + (isPrivateHost(parsed.hostname) ? `, or set ${ALLOW_INSECURE_HTTP_ENV}=1 if this really is a trusted local endpoint.` : `. ${ALLOW_INSECURE_HTTP_ENV} does not apply here: it covers private networks only, and this is a public address.`);
19595
19631
  }
19596
19632
  function resolveApiUrl(configuredApiUrl, env = process.env) {
19597
19633
  const fromEnv = env.CANDLE_API_URL?.trim();
@@ -19889,7 +19925,7 @@ function resolveProfileName(config, opts) {
19889
19925
  if (requested !== undefined) {
19890
19926
  if (!isValidProfileName(requested))
19891
19927
  return { ok: false, message: `Invalid profile name: ${requested}` };
19892
- if (!(requested in profiles)) {
19928
+ if (!Object.hasOwn(profiles, requested)) {
19893
19929
  return {
19894
19930
  ok: false,
19895
19931
  message: `No profile named "${requested}".${names.length ? `
@@ -19899,7 +19935,7 @@ ${listForHumans(profiles, config.activeProfile)}` : " Run: candle auth login --p
19899
19935
  }
19900
19936
  return { ok: true, name: requested };
19901
19937
  }
19902
- if (config.activeProfile && config.activeProfile in profiles)
19938
+ if (config.activeProfile && Object.hasOwn(profiles, config.activeProfile))
19903
19939
  return { ok: true, name: config.activeProfile };
19904
19940
  if (names.length === 0)
19905
19941
  return { ok: true, name: undefined };
@@ -19919,7 +19955,7 @@ function resolveProfileNameForLogin(config, opts) {
19919
19955
  return { ok: false, message: `Invalid profile name: ${requested}` };
19920
19956
  return { ok: true, name: requested };
19921
19957
  }
19922
- if (config.activeProfile && config.activeProfile in profiles)
19958
+ if (config.activeProfile && Object.hasOwn(profiles, config.activeProfile))
19923
19959
  return { ok: true, name: config.activeProfile };
19924
19960
  const names = Object.keys(profiles);
19925
19961
  return { ok: true, name: names.length === 1 ? names[0] : undefined };
@@ -20016,6 +20052,41 @@ function profileTable(config, now) {
20016
20052
  import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
20017
20053
  import { homedir } from "node:os";
20018
20054
  import { dirname, join } from "node:path";
20055
+
20056
+ // src/file-lock.ts
20057
+ import { open, rm, stat } from "node:fs/promises";
20058
+ var STALE_MS = 30000;
20059
+ var RETRY_MS = 25;
20060
+ var TIMEOUT_MS = 1e4;
20061
+ async function withFileLock(target, fn) {
20062
+ const lockPath = `${target}.lock`;
20063
+ const deadline = Date.now() + TIMEOUT_MS;
20064
+ for (;; ) {
20065
+ try {
20066
+ await (await open(lockPath, "wx")).close();
20067
+ break;
20068
+ } catch (error) {
20069
+ if (error.code !== "EEXIST")
20070
+ throw error;
20071
+ const age = await stat(lockPath).then((s) => Date.now() - s.mtimeMs).catch(() => 0);
20072
+ if (age > STALE_MS) {
20073
+ await rm(lockPath, { force: true });
20074
+ continue;
20075
+ }
20076
+ if (Date.now() > deadline) {
20077
+ throw new Error(`Timed out waiting for ${lockPath}. Another candle process is writing; if none is running, delete that file.`);
20078
+ }
20079
+ await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
20080
+ }
20081
+ }
20082
+ try {
20083
+ return await fn();
20084
+ } finally {
20085
+ await rm(lockPath, { force: true });
20086
+ }
20087
+ }
20088
+
20089
+ // src/secret-store.ts
20019
20090
  var SECRET_REFS = {
20020
20091
  deviceToken: "device_token",
20021
20092
  apiKey: "api_key"
@@ -20073,26 +20144,30 @@ class EncryptedFileSecretStore {
20073
20144
  }
20074
20145
  async set(ref, value) {
20075
20146
  const passphrase = await this.resolvePassphrase();
20076
- const contents = await this.readContents();
20077
- const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH_BYTES));
20078
- const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH_BYTES));
20079
- const key = await deriveKey(passphrase, salt, this.iterations);
20080
- const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(value));
20081
- contents[ref] = {
20082
- salt: toBase64(salt),
20083
- iv: toBase64(iv),
20084
- ciphertext: toBase64(new Uint8Array(ciphertext)),
20085
- iterations: this.iterations
20086
- };
20087
- await this.writeContents(contents);
20147
+ return withFileLock(this.path, async () => {
20148
+ const contents = await this.readContents();
20149
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH_BYTES));
20150
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH_BYTES));
20151
+ const key = await deriveKey(passphrase, salt, this.iterations);
20152
+ const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(value));
20153
+ contents[ref] = {
20154
+ salt: toBase64(salt),
20155
+ iv: toBase64(iv),
20156
+ ciphertext: toBase64(new Uint8Array(ciphertext)),
20157
+ iterations: this.iterations
20158
+ };
20159
+ await this.writeContents(contents);
20160
+ });
20088
20161
  }
20089
20162
  async delete(ref) {
20090
20163
  await this.resolvePassphrase();
20091
- const contents = await this.readContents();
20092
- if (!(ref in contents))
20093
- return;
20094
- delete contents[ref];
20095
- await this.writeContents(contents);
20164
+ return withFileLock(this.path, async () => {
20165
+ const contents = await this.readContents();
20166
+ if (!Object.hasOwn(contents, ref))
20167
+ return;
20168
+ delete contents[ref];
20169
+ await this.writeContents(contents);
20170
+ });
20096
20171
  }
20097
20172
  async resolvePassphrase() {
20098
20173
  if (this.cachedPassphrase !== undefined)
@@ -20128,7 +20203,7 @@ class EncryptedFileSecretStore {
20128
20203
  const dir = dirname(this.path);
20129
20204
  await mkdir(dir, { recursive: true });
20130
20205
  await chmod(dir, 448);
20131
- const tmpPath = `${this.path}.tmp`;
20206
+ const tmpPath = `${this.path}.${crypto.randomUUID()}.tmp`;
20132
20207
  await writeFile(tmpPath, JSON.stringify(contents, null, 2), { encoding: "utf8", mode: 384 });
20133
20208
  await chmod(tmpPath, 384);
20134
20209
  await rename(tmpPath, this.path);
@@ -20183,7 +20258,7 @@ async function resolveApiKey(deps, profile) {
20183
20258
  }
20184
20259
 
20185
20260
  // src/version.ts
20186
- var CLI_VERSION = "0.8.0";
20261
+ var CLI_VERSION = "0.8.1";
20187
20262
 
20188
20263
  // src/commands/auth.ts
20189
20264
  var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
@@ -20865,21 +20940,25 @@ async function keysCreate(args, ctx) {
20865
20940
  }
20866
20941
  const body = result.body;
20867
20942
  const apiKeyRef = ctx.profile ? profileSecretRef(ctx.profile, "apiKey") : SECRET_REFS.apiKey;
20868
- const existingKey = await deps.store.get(apiKeyRef);
20869
20943
  let stored = false;
20870
- if (!existingKey) {
20871
- await deps.store.set(apiKeyRef, body.key);
20872
- if (ctx.profile) {
20873
- await deps.updateProfile(ctx.profile, { keyPrefix: body.keyPrefix, scopes: body.scopes });
20874
- } else {
20875
- await deps.writeConfig({ keyPrefix: body.keyPrefix, scopes: body.scopes });
20944
+ let storeError;
20945
+ try {
20946
+ if (!await deps.store.get(apiKeyRef)) {
20947
+ await deps.store.set(apiKeyRef, body.key);
20948
+ if (ctx.profile) {
20949
+ await deps.updateProfile(ctx.profile, { keyPrefix: body.keyPrefix, scopes: body.scopes });
20950
+ } else {
20951
+ await deps.writeConfig({ keyPrefix: body.keyPrefix, scopes: body.scopes });
20952
+ }
20953
+ stored = true;
20876
20954
  }
20877
- stored = true;
20955
+ } catch (error) {
20956
+ storeError = error instanceof Error ? error.message : String(error);
20878
20957
  }
20879
20958
  if (json) {
20880
- deps.stdout.write(`${JSON.stringify({ ...body, stored })}
20959
+ deps.stdout.write(`${JSON.stringify({ ...body, stored, ...storeError ? { storeError } : {} })}
20881
20960
  `);
20882
- return 0;
20961
+ return storeError ? 1 : 0;
20883
20962
  }
20884
20963
  deps.stdout.write(`API key: ${body.key}
20885
20964
  `);
@@ -20889,14 +20968,22 @@ async function keysCreate(args, ctx) {
20889
20968
  `);
20890
20969
  deps.stdout.write(`Scopes: ${formatScopesForSummary(body.scopes)}
20891
20970
  `);
20971
+ if (storeError !== undefined) {
20972
+ deps.stderr.write(`
20973
+ WARNING: the key above was NOT stored in the ${deps.backend} store: ${storeError}
20974
+ ` + "It is live on your account. Save it now, or revoke it with: candle keys revoke " + `${body.keyPrefix}
20975
+ `);
20976
+ }
20892
20977
  if (!requestedScopes) {
20893
20978
  deps.stdout.write(`No --scopes given: the server granted the default scopes (swap:write excluded).
20894
20979
  `);
20895
20980
  }
20896
- deps.stdout.write(stored ? `Stored in the ${deps.backend} backend as the CLI's working key.
20981
+ if (storeError === undefined) {
20982
+ deps.stdout.write(stored ? `Stored in the ${deps.backend} backend as the CLI's working key.
20897
20983
  ` : `Not stored: the CLI already manages a different working key. This key belongs to whichever agent it was minted for.
20898
20984
  `);
20899
- return 0;
20985
+ }
20986
+ return storeError === undefined ? 0 : 1;
20900
20987
  }
20901
20988
  async function keysRevoke(args, ctx) {
20902
20989
  const { deps, apiUrl, json } = ctx;
@@ -21149,7 +21236,7 @@ async function profileAdd(args, ctx) {
21149
21236
  return 2;
21150
21237
  }
21151
21238
  const config = await deps.readConfig();
21152
- if (config.profiles?.[name]) {
21239
+ if (config.profiles !== undefined && Object.hasOwn(config.profiles, name)) {
21153
21240
  writeLocalFailure(deps, {
21154
21241
  code: "PROFILE_EXISTS",
21155
21242
  message: `Profile "${name}" already exists.`,
@@ -21181,7 +21268,7 @@ async function profileUse(args, ctx) {
21181
21268
  return 2;
21182
21269
  }
21183
21270
  const config = await deps.readConfig();
21184
- const profile = config.profiles?.[name];
21271
+ const profile = config.profiles !== undefined && Object.hasOwn(config.profiles, name) ? config.profiles[name] : undefined;
21185
21272
  if (!profile) {
21186
21273
  const names = Object.keys(config.profiles ?? {}).join(", ") || "(none)";
21187
21274
  writeLocalFailure(deps, {
@@ -24768,11 +24855,16 @@ function buildCipherSuite() {
24768
24855
  function parseSolanaSecret(input) {
24769
24856
  const trimmed = input.trim();
24770
24857
  if (!trimmed.startsWith("[")) {
24858
+ let decoded;
24771
24859
  try {
24772
- return base58.decode(trimmed);
24860
+ decoded = base58.decode(trimmed);
24773
24861
  } catch {
24774
24862
  throw new Error("Invalid Solana private key: expected a base58 string or an id.json byte array.");
24775
24863
  }
24864
+ if (decoded.length !== 64) {
24865
+ throw new Error(`Invalid Solana private key: expected 64 bytes, got ${decoded.length}. A 32-byte value is the ` + "SEED, not the keypair; export the full secret key, which is what solana-keygen writes to id.json.");
24866
+ }
24867
+ return decoded;
24776
24868
  }
24777
24869
  const parsed = (() => {
24778
24870
  try {
@@ -24790,8 +24882,8 @@ function parseSolanaSecret(input) {
24790
24882
  function decodeWalletPrivateKey(chain2, privateKey) {
24791
24883
  if (chain2 === "evm") {
24792
24884
  const hex2 = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
24793
- if (hex2.length === 0 || hex2.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex2)) {
24794
- throw new Error('Invalid EVM private key: expected a hex string (optionally "0x"-prefixed)');
24885
+ if (hex2.length !== 64 || !/^[0-9a-fA-F]+$/.test(hex2)) {
24886
+ throw new Error("Invalid EVM private key: expected 32 bytes as 64 hex characters " + `(optionally "0x"-prefixed), got ${hex2.length} characters`);
24795
24887
  }
24796
24888
  return Uint8Array.from(Buffer.from(hex2, "hex"));
24797
24889
  }
@@ -25315,7 +25407,7 @@ async function writeKeystoreFile(path, contents) {
25315
25407
  const dir = dirname3(path);
25316
25408
  await mkdir2(dir, { recursive: true });
25317
25409
  await chmod2(dir, 448);
25318
- const tmpPath = `${path}.tmp`;
25410
+ const tmpPath = `${path}.${crypto.randomUUID()}.tmp`;
25319
25411
  await writeFile2(tmpPath, contents, { encoding: "utf8", mode: 384 });
25320
25412
  await chmod2(tmpPath, 384);
25321
25413
  await rename2(tmpPath, path);
@@ -27919,7 +28011,16 @@ async function walletsGenerate(args, ctx) {
27919
28011
  let existingRaw = null;
27920
28012
  try {
27921
28013
  existingRaw = await deps.readFile(keystorePath);
27922
- } catch {
28014
+ } catch (error) {
28015
+ const code = error?.code;
28016
+ if (code !== undefined && code !== "ENOENT") {
28017
+ writeLocalFailure(deps, {
28018
+ code: "KEYSTORE_UNREADABLE",
28019
+ message: `Could not read ${keystorePath}: ${error instanceof Error ? error.message : error}`,
28020
+ suggestion: "Refusing to continue: a keystore may exist at that path, and overwriting it would destroy the only copy of its keys."
28021
+ }, json);
28022
+ return 1;
28023
+ }
27923
28024
  existingRaw = null;
27924
28025
  }
27925
28026
  if (existingRaw !== null && !resuming) {
@@ -28045,14 +28146,25 @@ Sealed to ${keystorePath}
28045
28146
  if (alreadyExists) {
28046
28147
  const found = await lookupByAddress(entry.address, apiKey, ctx);
28047
28148
  if (found) {
28149
+ const promoted = await promoteStagedSigner(deps, entry, found._id);
28048
28150
  entry.imported = true;
28049
- entry.privyWalletId = found._id;
28151
+ entry.linkedWalletId = found._id;
28050
28152
  entry.importedAt = new Date().toISOString();
28051
28153
  await persist(store, keystorePath);
28052
- if (!json)
28053
- deps.stdout.write(` [${entry.index}] ${entry.address} already imported, reconciled
28154
+ if (promoted.ok) {
28155
+ if (!json) {
28156
+ deps.stdout.write(` [${entry.index}] ${entry.address} already imported, reconciled as ${found._id}
28054
28157
  `);
28055
- continue;
28158
+ }
28159
+ continue;
28160
+ }
28161
+ failures++;
28162
+ writeLocalFailure(deps, {
28163
+ code: "SIGNER_MISSING",
28164
+ message: `Wallet ${entry.address} exists as ${found._id}, but ${promoted.message}.`,
28165
+ suggestion: `Revoke it (candle wallets revoke ${found._id}) and run: candle wallets generate --resume`
28166
+ }, json);
28167
+ break;
28056
28168
  }
28057
28169
  }
28058
28170
  failures++;
@@ -28068,6 +28180,7 @@ Sealed to ${keystorePath}
28068
28180
  break;
28069
28181
  }
28070
28182
  entry.imported = true;
28183
+ entry.linkedWalletId = flow.submitted.id;
28071
28184
  entry.privyWalletId = flow.submitted.privyWalletId;
28072
28185
  entry.importedAt = new Date().toISOString();
28073
28186
  await persist(store, keystorePath);
@@ -28106,6 +28219,22 @@ ${imported}/${store.entries.length} imported.
28106
28219
  `);
28107
28220
  return 0;
28108
28221
  }
28222
+ async function promoteStagedSigner(deps, entry, linkedWalletId) {
28223
+ const committedRef = walletSignerRef(linkedWalletId);
28224
+ if (await deps.store.get(committedRef) !== null)
28225
+ return { ok: true };
28226
+ const pendingRef = importPendingSignerRef(entry.chain, entry.address);
28227
+ const staged = await deps.store.get(pendingRef);
28228
+ if (staged === null) {
28229
+ return {
28230
+ ok: false,
28231
+ message: `its signer is on neither "${committedRef}" nor "${pendingRef}" in the ${deps.backend} store`
28232
+ };
28233
+ }
28234
+ await deps.store.set(committedRef, staged);
28235
+ await deps.store.delete(pendingRef).catch(() => {});
28236
+ return { ok: true };
28237
+ }
28109
28238
  async function persist(store, path) {
28110
28239
  await writeKeystoreFile(path, await serializeKeystore(store.entries, store.key, store.salt, store.iterations));
28111
28240
  }
@@ -28124,7 +28253,7 @@ async function lookupByAddress(address, apiKey, ctx) {
28124
28253
  }
28125
28254
 
28126
28255
  // src/config.ts
28127
- import { chmod as chmod3, mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile3 } from "node:fs/promises";
28256
+ import { chmod as chmod3, mkdir as mkdir3, readFile as readFile2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
28128
28257
  import { homedir as homedir4 } from "node:os";
28129
28258
  import { join as join6 } from "node:path";
28130
28259
  function configDir2() {
@@ -28159,7 +28288,7 @@ async function updateProfile(name, patch) {
28159
28288
  }
28160
28289
  async function clearConfig() {
28161
28290
  try {
28162
- await rm(configFilePath());
28291
+ await rm2(configFilePath());
28163
28292
  } catch (err) {
28164
28293
  if (err.code !== "ENOENT")
28165
28294
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup health from the terminal",
5
5
  "type": "module",
6
6
  "bin": {