@hardfin/cli 0.1.1-dev.44 → 0.1.1-dev.46

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 CHANGED
@@ -16,6 +16,8 @@ server, and every command also accepts an API key.
16
16
  npm install -g @hardfin/cli
17
17
  ```
18
18
 
19
+ It needs Node 20.12 or later.
20
+
19
21
  Previews of unreleased work are published from the `dev` branch.
20
22
 
21
23
  ```sh
@@ -183,7 +185,9 @@ hardfin logout # forget this machine, and ask Hardfin to revoke it
183
185
  ```
184
186
 
185
187
  An API key is still the way to run unattended. `--api-key`, or `HARDFIN_API_KEY`, wins over
186
- a stored sign in, so CI and an agent sandbox need no browser. The flag beats the variable.
188
+ a stored sign in, so CI and an agent sandbox need no browser. The flag beats the variable. Prefer the
189
+ variable, because a command line is visible to other programs on the machine and is kept in
190
+ an agent's transcript.
187
191
 
188
192
  An API key is held to two rules before it is sent:
189
193
 
@@ -202,7 +206,8 @@ how a local build is set up. Pass `--api-url` to send a key you set to that fold
202
206
  2. Opens a loopback listener on `127.0.0.1`, over IPv4, on whatever port the machine hands out
203
207
  3. Opens the browser to approve this CLI, with a PKCE challenge and a state value
204
208
  4. Prints the URL, and waits at the keyboard as well as on the listener
205
- 5. Checks the state and the `iss` it came back with, then trades the code for tokens
209
+ 5. Checks the state and the `iss` it came back with, then trades the code for tokens. Every
210
+ endpoint the authorization server named must be https, or this machine
206
211
  6. Keeps the refresh token, and nothing else
207
212
 
208
213
  While it waits, the terminal takes two things.
@@ -494,7 +499,8 @@ Only defects are reported. None of these are:
494
499
  - a network failure, or a server that cannot be reached
495
500
  - a mistake on the command line or in a settings file
496
501
  - being signed out, or a sign in that was cancelled or refused
497
- - the machine refusing a file, such as a full disk or a missing permission
502
+ - the machine refusing a file, such as a full disk or a missing permission, unless the file
503
+ is one the CLI installs, which makes it a broken package
498
504
  - anything from a run with no API key and no stored sign in, except a defect in `hardfin login`
499
505
 
500
506
  The last rule is there because package scanners install the CLI and run it in sandboxes. They
@@ -505,8 +511,9 @@ A report carries the error, its stack trace, and what `hardfin status` would pri
505
511
  moment. The command is named, and its flags are listed by name without their values.
506
512
 
507
513
  A report never carries tokens, API keys, argument or flag values, request or response bodies,
508
- people's or organizations' names, your hostname, or your home directory. A person and an
509
- organization appear only as their Hardfin keys.
514
+ people's or organizations' names, an API key's label, your hostname, or your home directory.
515
+ A person and an organization appear only as their Hardfin keys and IDs. The report names the
516
+ few parts of an identity it carries, so a part the API adds later is left out.
510
517
 
511
518
  | To | Set |
512
519
  | ------------------------------------------------- | ------------------------------------------ |
@@ -532,9 +539,11 @@ settings, and the one nearest the top wins.
532
539
 
533
540
  An exported variable beats `.env`, because the CLI leaves a variable that is already set alone.
534
541
 
535
- An env file sets only variables that begin with `HARDFIN_`. Anything else in it is ignored,
536
- so a `.env` in a folder you cloned cannot set a variable such as `NODE_OPTIONS` for the
537
- programs the CLI starts.
542
+ An env file sets the four settings: `HARDFIN_API_URL`, `HARDFIN_API_KEY`, `HARDFIN_CLIENT_ID`
543
+ and `HARDFIN_ISSUER_URL`. It may also turn error reports off, with `DO_NOT_TRACK=1` or
544
+ `HARDFIN_TELEMETRY=0`. Anything else in it is ignored, so a `.env` in a folder you cloned
545
+ cannot set `NODE_OPTIONS` for the programs the CLI starts, or send error reports elsewhere
546
+ with `HARDFIN_SENTRY_DSN`. Export a variable yourself to set it.
538
547
 
539
548
  ### What a local build writes
540
549
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="641b3aa6-1d59-5f9c-b2f3-0bf132c35831")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6c054d7b-a0a8-526c-90d7-5bbec53675dd")}catch(e){}}();
4
4
  import module from "node:module";
5
5
  //#region src/cli.ts
6
6
  module.enableCompileCache?.();
@@ -14,7 +14,7 @@ process.on("unhandledRejection", (error) => void toCrash(error));
14
14
  try {
15
15
  const answer = await toQuickAnswer(process.argv.slice(2));
16
16
  if (answer === void 0) {
17
- const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-CgT1Kr8L.js");
17
+ const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-Da7dZSnW.js");
18
18
  try {
19
19
  await toCli().parseAsync(process.argv);
20
20
  } catch (error) {
@@ -31,10 +31,13 @@ try {
31
31
  async function toCrash(error) {
32
32
  if (isCrashing) return;
33
33
  isCrashing = true;
34
- process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
34
+ const isJSON = process.argv.includes("--json") || !process.stdout.isTTY;
35
35
  try {
36
- const { toReported } = await import("./program-CgT1Kr8L.js");
37
- await toReported(error, false, void 0, "fatal");
36
+ const { toReported, writeCrash } = await import("./program-Da7dZSnW.js");
37
+ writeCrash(error, isJSON);
38
+ await toReported(error, isJSON, void 0, "fatal");
39
+ } catch {
40
+ process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
38
41
  } finally {
39
42
  process.exit(1);
40
43
  }
@@ -51,4 +54,4 @@ async function toQuickAnswer(args) {
51
54
  export {};
52
55
 
53
56
  //# sourceMappingURL=cli.js.map
54
- //# debugId=641b3aa6-1d59-5f9c-b2f3-0bf132c35831
57
+ //# debugId=6c054d7b-a0a8-526c-90d7-5bbec53675dd
@@ -7,7 +7,7 @@
7
7
  },
8
8
  {
9
9
  "name": "api-key",
10
- "description": "An API key to call with, in place of HARDFIN_API_KEY or a sign in",
10
+ "description": "An API key to call with. Prefer HARDFIN_API_KEY, because a command line is visible to other programs",
11
11
  "valueName": "key"
12
12
  },
13
13
  {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2c607b02-61ea-5376-957f-d434d5692ac5")}catch(e){}}();
4
- import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BN3gyBpR.js";
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e0ad2183-1cb1-5831-b5d1-b96dcf795d93")}catch(e){}}();
4
+ import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BZ0egWxC.js";
5
5
  import { n as toAnswer$1, r as toCompletion, t as PATH_VALUES } from "./completion-tree-DLC6ARLR.js";
6
6
  import { createRequire } from "node:module";
7
7
  import { Command, CommanderError, Option } from "commander";
@@ -9,11 +9,11 @@ import { z } from "zod";
9
9
  import { accessSync, chmodSync, closeSync, constants, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
10
10
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
11
11
  import { parseEnv } from "node:util";
12
+ import { fileURLToPath } from "node:url";
12
13
  import { createHash, randomBytes, randomUUID } from "node:crypto";
13
14
  import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
14
15
  import { spawn, spawnSync } from "node:child_process";
15
16
  import { createInterface } from "node:readline/promises";
16
- import { fileURLToPath } from "node:url";
17
17
  import { createServer } from "node:http";
18
18
  import { createInterface as createInterface$1 } from "node:readline";
19
19
  //#region src/output/writer.ts
@@ -27,10 +27,21 @@ function toText(value) {
27
27
  if (value === void 0 || value === null) return "";
28
28
  return JSON.stringify(value);
29
29
  }
30
+ /** Every control character but a newline and a tab, which are the two a line of text needs. */
31
+ const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
32
+ /**
33
+ * toPrintable makes text safe to show at a terminal. A name or a message from the API can
34
+ * hold an escape sequence, which a terminal would act on. Each such character is written
35
+ * as its code, so the text reads as what it holds.
36
+ */
37
+ function toPrintable(text) {
38
+ return text.replace(CONTROL, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
39
+ }
30
40
  /** writeData prints a command's result on stdout. */
31
41
  function writeData(value) {
32
42
  if (typeof value === "string") {
33
- process.stdout.write(value.endsWith("\n") ? value : `${value}\n`);
43
+ const text = toPrintable(value);
44
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
34
45
  return;
35
46
  }
36
47
  process.stdout.write(`${JSON.stringify(value ?? null, null, 2)}\n`);
@@ -38,9 +49,9 @@ function writeData(value) {
38
49
  /** writeFailure prints why a command failed on stderr, with a code only a machine reading JSON sees. */
39
50
  function writeFailure(code, message, isJSON, errors, requestId, status) {
40
51
  if (!isJSON) {
41
- process.stderr.write(`error: ${message}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
42
- for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${entry.error} (${entry.statusCode})\n`);
43
- if (requestId) process.stderr.write(` request ${requestId}\n`);
52
+ process.stderr.write(`error: ${toPrintable(message)}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
53
+ for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${toPrintable(entry.error)} (${entry.statusCode})\n`);
54
+ if (requestId) process.stderr.write(` request ${toPrintable(requestId)}\n`);
44
55
  return;
45
56
  }
46
57
  const payload = {
@@ -155,6 +166,77 @@ var TimedOut = class extends ExpectedFailure {
155
166
  this.name = "TimedOut";
156
167
  }
157
168
  };
169
+ /** The values that mean no, for a variable that turns something on or off. */
170
+ const OFF = /* @__PURE__ */ new Set([
171
+ "0",
172
+ "false",
173
+ "off",
174
+ "no"
175
+ ]);
176
+ /**
177
+ * toTelemetry decides whether defects are reported. A person's opt out wins over everything,
178
+ * and a CLI run from a checkout reports only when its developer asks it to.
179
+ */
180
+ function toTelemetry(env = process.env, isCheckout = isRunFromCheckout()) {
181
+ const dsn = env["HARDFIN_SENTRY_DSN"] || "https://a4819e652e9fa63c0ab67e9febe44565@o1388102.ingest.us.sentry.io/4512176532684800";
182
+ const base = {
183
+ dsn,
184
+ environment: isCheckout ? "local" : version$1.includes("-dev.") ? "preview" : "production",
185
+ release: toRelease(version$1)
186
+ };
187
+ if (env["DO_NOT_TRACK"] && !OFF.has(env["DO_NOT_TRACK"].toLowerCase())) return {
188
+ ...base,
189
+ enabled: false,
190
+ from: "DO_NOT_TRACK"
191
+ };
192
+ const asked = env["HARDFIN_TELEMETRY"]?.toLowerCase();
193
+ if (asked !== void 0 && OFF.has(asked)) return {
194
+ ...base,
195
+ enabled: false,
196
+ from: "HARDFIN_TELEMETRY"
197
+ };
198
+ if (!isDsn(dsn)) return {
199
+ ...base,
200
+ enabled: false,
201
+ from: "HARDFIN_SENTRY_DSN is not a valid DSN"
202
+ };
203
+ if (asked !== void 0) return {
204
+ ...base,
205
+ enabled: true,
206
+ from: "HARDFIN_TELEMETRY"
207
+ };
208
+ return isCheckout ? {
209
+ ...base,
210
+ enabled: false,
211
+ from: "run from a checkout"
212
+ } : {
213
+ ...base,
214
+ enabled: true,
215
+ from: "default"
216
+ };
217
+ }
218
+ /**
219
+ * isRunFromCheckout answers whether this CLI runs from a clone of its repository, as npm link
220
+ * and node dist/cli.js do. The published package holds dist and nothing beside it.
221
+ */
222
+ function isRunFromCheckout() {
223
+ return existsSync(fileURLToPath(new URL("../src", import.meta.url)));
224
+ }
225
+ /** isDsn answers whether a DSN can be read as the URL of a Sentry project. */
226
+ function isDsn(dsn) {
227
+ try {
228
+ return new URL(dsn).host !== "";
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+ /**
234
+ * toRelease names a version as Sentry knows it. Sentry refuses a slash in a release name, so
235
+ * the scoped package name @hardfin/cli cannot be used, and hardfin-cli stands in for it.
236
+ */
237
+ function toRelease(of) {
238
+ return `hardfin-cli@${of}`;
239
+ }
158
240
  //#endregion
159
241
  //#region src/config/settings.ts
160
242
  /** The API version this build was written against, sent on every request. */
@@ -225,8 +307,23 @@ function toFileSettings(directory) {
225
307
  }
226
308
  return result.data;
227
309
  }
228
- /** The prefix of every variable this CLI reads, which is all an env file may set. */
229
- const VARIABLE_PREFIX = "HARDFIN_";
310
+ /** The variables an env file may set, which are the settings and nothing else the CLI reads. */
311
+ const SETTING_VARIABLES = /* @__PURE__ */ new Set([
312
+ "HARDFIN_API_URL",
313
+ "HARDFIN_API_KEY",
314
+ "HARDFIN_CLIENT_ID",
315
+ "HARDFIN_ISSUER_URL"
316
+ ]);
317
+ /**
318
+ * isTakenFromFile answers whether an env file may set a variable. A file in a folder supplies
319
+ * the settings, and may turn error reports off. Where reports go, where a sign in is kept,
320
+ * and anything a started program reads are for the person to set themselves.
321
+ */
322
+ function isTakenFromFile(name, value) {
323
+ if (SETTING_VARIABLES.has(name)) return true;
324
+ if (name === "DO_NOT_TRACK") return !OFF.has(value.toLowerCase());
325
+ return name === "HARDFIN_TELEMETRY" && OFF.has(value.toLowerCase());
326
+ }
230
327
  /**
231
328
  * loadEnvFile reads a .env beside the command, so a local build needs no exports, and
232
329
  * answers which variables it supplied. An exported variable is left alone, so a shell
@@ -243,7 +340,7 @@ function loadEnvFile(directory) {
243
340
  }
244
341
  const supplied = /* @__PURE__ */ new Set();
245
342
  for (const [name, value] of Object.entries(held)) {
246
- if (!name.startsWith(VARIABLE_PREFIX) || value === void 0 || process.env[name] !== void 0) continue;
343
+ if (value === void 0 || process.env[name] !== void 0 || !isTakenFromFile(name, value)) continue;
247
344
  process.env[name] = value;
248
345
  supplied.add(name);
249
346
  }
@@ -269,6 +366,19 @@ var Unreachable = class extends ExpectedFailure {
269
366
  this.name = "Unreachable";
270
367
  }
271
368
  };
369
+ /** isLoopback reports whether a host is this machine. */
370
+ function isLoopback(hostname) {
371
+ return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "localhost";
372
+ }
373
+ /** isSecure answers whether a URL is https, or is this machine, where nothing sits in between. */
374
+ function isSecure(url) {
375
+ try {
376
+ const parsed = new URL(url);
377
+ return parsed.protocol === "https:" || parsed.protocol === "http:" && isLoopback(parsed.hostname);
378
+ } catch {
379
+ return false;
380
+ }
381
+ }
272
382
  const OFFLINE = "Couldn't connect to Hardfin. Check your internet connection and try again.";
273
383
  const INSECURE = "Couldn't connect to Hardfin securely, so nothing was sent. A work network or security software may be blocking the connection. If this keeps happening, contact your IT team.";
274
384
  /** The reasons Node gives, by the code on the error behind fetch's own. */
@@ -660,9 +770,23 @@ async function toMetadata(issuer, signal) {
660
770
  const parsed = AuthorizationServerMetadata.safeParse(body);
661
771
  if (!parsed.success) throw new DiscoveryFailure(`${url} does not describe an authorization server`);
662
772
  if (parsed.data.issuer.replace(/\/+$/, "") !== issuer.replace(/\/+$/, "")) throw new DiscoveryFailure(`${url} names issuer ${parsed.data.issuer}, which is not the host it was read from`);
773
+ const plain = toPlainEndpoint(parsed.data);
774
+ if (plain) throw new DiscoveryFailure(`${url} names ${plain}, which is not https, so nothing is sent to it`);
663
775
  keep(issuer, parsed.data);
664
776
  return parsed.data;
665
777
  }
778
+ /**
779
+ * toPlainEndpoint names an endpoint that would carry a code or a refresh token over a plain
780
+ * connection. The document chooses every endpoint, so each is checked before any is used.
781
+ */
782
+ function toPlainEndpoint(metadata) {
783
+ return [
784
+ metadata.authorization_endpoint,
785
+ metadata.token_endpoint,
786
+ metadata.revocation_endpoint,
787
+ metadata.device_authorization_endpoint
788
+ ].find((endpoint) => endpoint !== void 0 && !isSecure(endpoint));
789
+ }
666
790
  /** toCachePath names where one server's document is kept, beside the credentials. */
667
791
  function toCachePath(issuer) {
668
792
  const name = createHash("sha256").update(issuer).digest("hex").slice(0, 16);
@@ -675,7 +799,7 @@ function toCached(issuer) {
675
799
  const held = JSON.parse(readFileSync(path, "utf8"));
676
800
  if ((held.readAt ?? 0) + CACHE_MS < Date.now()) return;
677
801
  const parsed = AuthorizationServerMetadata.safeParse(held.document);
678
- return parsed.success ? parsed.data : void 0;
802
+ return parsed.success && !toPlainEndpoint(parsed.data) ? parsed.data : void 0;
679
803
  } catch {
680
804
  return;
681
805
  }
@@ -929,10 +1053,6 @@ function toHost$1(url) {
929
1053
  return;
930
1054
  }
931
1055
  }
932
- /** isLoopback reports whether a host is this machine. */
933
- function isLoopback(hostname) {
934
- return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "localhost";
935
- }
936
1056
  /**
937
1057
  * toHeldCredential answers a credential without asking the server for one: the API key, or
938
1058
  * an access token this process already holds. An error report uses it, because a refresh
@@ -1051,6 +1171,35 @@ function toStatusMessage(status) {
1051
1171
  return `the request failed with status ${status}`;
1052
1172
  }
1053
1173
  //#endregion
1174
+ //#region src/system/package.ts
1175
+ /** The folders this package ships, beside its manifest. */
1176
+ const SHIPPED = [
1177
+ "dist",
1178
+ "skills",
1179
+ "templates"
1180
+ ];
1181
+ /** toPackageDirectory finds this package's own folder, which holds the skills and templates it ships. */
1182
+ function toPackageDirectory(from = dirname(fileURLToPath(import.meta.url))) {
1183
+ for (let directory = from;; directory = dirname(directory)) {
1184
+ const manifest = join(directory, "package.json");
1185
+ if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@hardfin/cli") return directory;
1186
+ if (dirname(directory) === directory) throw new Error("the @hardfin/cli package folder could not be found, so its skills cannot be copied");
1187
+ }
1188
+ }
1189
+ /**
1190
+ * isShipped answers whether a path is one of the files this package installs. A file of the
1191
+ * CLI's own that cannot be read is a broken install, not the person's machine refusing them.
1192
+ */
1193
+ function isShipped(path, packageDirectory) {
1194
+ let directory;
1195
+ try {
1196
+ directory = packageDirectory ?? toPackageDirectory();
1197
+ } catch {
1198
+ return false;
1199
+ }
1200
+ return path === join(directory, "package.json") || SHIPPED.some((folder) => path === join(directory, folder) || path.startsWith(join(directory, folder) + sep));
1201
+ }
1202
+ //#endregion
1054
1203
  //#region src/report/classify.ts
1055
1204
  /**
1056
1205
  * The codes Node gives when the machine refuses something: permissions, a full or read-only
@@ -1077,8 +1226,9 @@ function isReportable(error) {
1077
1226
  }
1078
1227
  /** isMachineRefusal answers whether an error is the machine refusing a file or a folder. */
1079
1228
  function isMachineRefusal(error) {
1080
- const code = error instanceof Error ? error.code : void 0;
1081
- return typeof code === "string" && ENVIRONMENT_CODES.has(code);
1229
+ const { code, path } = error instanceof Error ? error : {};
1230
+ if (typeof code !== "string" || !ENVIRONMENT_CODES.has(code)) return false;
1231
+ return !(typeof path === "string" && isShipped(path));
1082
1232
  }
1083
1233
  //#endregion
1084
1234
  //#region src/output/failure.ts
@@ -1170,7 +1320,7 @@ function toGuide(commands, version) {
1170
1320
  "",
1171
1321
  "## Before calling",
1172
1322
  "",
1173
- "- Authenticate with `--api-key`, by setting `HARDFIN_API_KEY`, or by running `hardfin login`",
1323
+ "- Authenticate by running `hardfin login`, or by setting `HARDFIN_API_KEY` in the environment. Do not pass a key as `--api-key`, because a command line is kept in transcripts and shown to other programs",
1174
1324
  "- `hardfin login` writes one JSON line on stderr before it waits. Show the person its `url`, or its `userCode` and `verificationUri`, and let the command run for `waitSeconds`",
1175
1325
  "- Every command prints JSON on stdout and diagnostics on stderr",
1176
1326
  "- Output is JSON whenever stdout is not a terminal, so no flag is needed when calling from code",
@@ -2213,14 +2363,6 @@ function toFiles(directory) {
2213
2363
  return statSync(path).isDirectory() ? toFiles(path) : [path];
2214
2364
  });
2215
2365
  }
2216
- /** toPackageDirectory finds this package's own folder, which holds the skills and templates it ships. */
2217
- function toPackageDirectory(from = dirname(fileURLToPath(import.meta.url))) {
2218
- for (let directory = from;; directory = dirname(directory)) {
2219
- const manifest = join(directory, "package.json");
2220
- if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@hardfin/cli") return directory;
2221
- if (dirname(directory) === directory) throw new Error("the @hardfin/cli package folder could not be found, so its skills cannot be copied");
2222
- }
2223
- }
2224
2366
  //#endregion
2225
2367
  //#region src/command/types.generated.ts
2226
2368
  /** API_TYPES is the TypeScript hardfin types prints. */
@@ -2681,8 +2823,8 @@ async function runDeviceLogin(input, metadata, scope) {
2681
2823
  });
2682
2824
  else process.stderr.write([
2683
2825
  "",
2684
- ` Code ${device.userCode}`,
2685
- ` At ${device.verificationUri}`,
2826
+ ` Code ${toPrintable(device.userCode)}`,
2827
+ ` At ${toPrintable(device.verificationUri)}`,
2686
2828
  "",
2687
2829
  opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
2688
2830
  ].join("\n"));
@@ -3126,76 +3268,6 @@ function toReal(path) {
3126
3268
  const parent = dirname(path);
3127
3269
  return parent === path ? path : join(toReal(parent), basename(path));
3128
3270
  }
3129
- const OFF = /* @__PURE__ */ new Set([
3130
- "0",
3131
- "false",
3132
- "off",
3133
- "no"
3134
- ]);
3135
- /**
3136
- * toTelemetry decides whether defects are reported. A person's opt out wins over everything,
3137
- * and a CLI run from a checkout reports only when its developer asks it to.
3138
- */
3139
- function toTelemetry(env = process.env, isCheckout = isRunFromCheckout()) {
3140
- const dsn = env["HARDFIN_SENTRY_DSN"] || "https://a4819e652e9fa63c0ab67e9febe44565@o1388102.ingest.us.sentry.io/4512176532684800";
3141
- const base = {
3142
- dsn,
3143
- environment: isCheckout ? "local" : version$1.includes("-dev.") ? "preview" : "production",
3144
- release: toRelease(version$1)
3145
- };
3146
- if (env["DO_NOT_TRACK"] && !OFF.has(env["DO_NOT_TRACK"].toLowerCase())) return {
3147
- ...base,
3148
- enabled: false,
3149
- from: "DO_NOT_TRACK"
3150
- };
3151
- const asked = env["HARDFIN_TELEMETRY"]?.toLowerCase();
3152
- if (asked !== void 0 && OFF.has(asked)) return {
3153
- ...base,
3154
- enabled: false,
3155
- from: "HARDFIN_TELEMETRY"
3156
- };
3157
- if (!isDsn(dsn)) return {
3158
- ...base,
3159
- enabled: false,
3160
- from: "HARDFIN_SENTRY_DSN is not a valid DSN"
3161
- };
3162
- if (asked !== void 0) return {
3163
- ...base,
3164
- enabled: true,
3165
- from: "HARDFIN_TELEMETRY"
3166
- };
3167
- return isCheckout ? {
3168
- ...base,
3169
- enabled: false,
3170
- from: "run from a checkout"
3171
- } : {
3172
- ...base,
3173
- enabled: true,
3174
- from: "default"
3175
- };
3176
- }
3177
- /**
3178
- * isRunFromCheckout answers whether this CLI runs from a clone of its repository, as npm link
3179
- * and node dist/cli.js do. The published package holds dist and nothing beside it.
3180
- */
3181
- function isRunFromCheckout() {
3182
- return existsSync(fileURLToPath(new URL("../src", import.meta.url)));
3183
- }
3184
- /** isDsn answers whether a DSN can be read as the URL of a Sentry project. */
3185
- function isDsn(dsn) {
3186
- try {
3187
- return new URL(dsn).host !== "";
3188
- } catch {
3189
- return false;
3190
- }
3191
- }
3192
- /**
3193
- * toRelease names a version as Sentry knows it. Sentry refuses a slash in a release name, so
3194
- * the scoped package name @hardfin/cli cannot be used, and hardfin-cli stands in for it.
3195
- */
3196
- function toRelease(of) {
3197
- return `hardfin-cli@${of}`;
3198
- }
3199
3271
  //#endregion
3200
3272
  //#region src/system/host.ts
3201
3273
  const BYTES_PER_GB = 1024 ** 3;
@@ -3502,7 +3574,7 @@ async function report(error, occasion, resolved) {
3502
3574
  try {
3503
3575
  const status = settings ? await toStatus(settings) : void 0;
3504
3576
  const secrets = [settings?.settings.apiKey, settings ? toCredential(settings.settings.issuerUrl)?.refreshToken : void 0];
3505
- return await (await import("./sentry-BM9Rvhhy.js")).send({
3577
+ return await (await import("./sentry-CC0H3uMB.js")).send({
3506
3578
  error,
3507
3579
  occasion,
3508
3580
  telemetry,
@@ -6843,6 +6915,10 @@ async function toExitCode(globals, command, args, flags) {
6843
6915
  return error instanceof ConfigFailure ? ExitCode.USAGE : ExitCode.ERROR;
6844
6916
  }
6845
6917
  }
6918
+ /** writeCrash answers an error nothing caught, in the form the caller reads. */
6919
+ function writeCrash(error, isJSON) {
6920
+ writeFailure(toErrorCode(error), error instanceof Error ? error.message : String(error), isJSON);
6921
+ }
6846
6922
  /** toReported reports a defect, and tells a person at a terminal that it was reported. */
6847
6923
  async function toReported(error, isJSON, resolved, level = "error") {
6848
6924
  const invocation = toInvocation(commands, process.argv.slice(2));
@@ -6856,7 +6932,7 @@ async function toReported(error, isJSON, resolved, level = "error") {
6856
6932
  if (id && !isJSON) process.stderr.write(`This looks like a problem in hardfin itself, and it has been reported to the Hardfin team. Reference: ${id}\n`);
6857
6933
  }
6858
6934
  //#endregion
6859
- export { toCli, toParseExitCode, toReported, wasReportAttempted };
6935
+ export { toCli, toParseExitCode, toReported, wasReportAttempted, writeCrash };
6860
6936
 
6861
- //# sourceMappingURL=program-CgT1Kr8L.js.map
6862
- //# debugId=2c607b02-61ea-5376-957f-d434d5692ac5
6937
+ //# sourceMappingURL=program-Da7dZSnW.js.map
6938
+ //# debugId=e0ad2183-1cb1-5831-b5d1-b96dcf795d93
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3af5060a-b6fe-5f2e-9661-01f4460a7b04")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="76640861-61e5-5004-ae78-dee93e00338f")}catch(e){}}();
4
4
  import { z } from "zod";
5
5
  import { homedir } from "node:os";
6
6
  //#region src/command/registry.ts
@@ -26,7 +26,7 @@ const GLOBAL_FLAGS = [
26
26
  },
27
27
  {
28
28
  name: "api-key",
29
- description: "An API key to call with, in place of HARDFIN_API_KEY or a sign in",
29
+ description: "An API key to call with. Prefer HARDFIN_API_KEY, because a command line is visible to other programs",
30
30
  valueName: "key",
31
31
  schema: z.string()
32
32
  },
@@ -118,5 +118,5 @@ function toFlag(flags, word) {
118
118
  //#endregion
119
119
  export { defineCommand as a, GLOBAL_FLAGS as i, toScrubbed as n, ExitCode as r, toInvocation as t };
120
120
 
121
- //# sourceMappingURL=scrub-BN3gyBpR.js.map
122
- //# debugId=3af5060a-b6fe-5f2e-9661-01f4460a7b04
121
+ //# sourceMappingURL=scrub-BZ0egWxC.js.map
122
+ //# debugId=76640861-61e5-5004-ae78-dee93e00338f
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="35c80804-dcd8-5662-9f00-9c8ff04f286a")}catch(e){}}();
4
- import { n as toScrubbed } from "./scrub-BN3gyBpR.js";
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="8beb7849-4488-5315-88c8-ae70cca2939f")}catch(e){}}();
4
+ import { n as toScrubbed } from "./scrub-BZ0egWxC.js";
5
5
  import { dirname } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { request } from "node:http";
@@ -4331,33 +4331,48 @@ function toIdentity(status) {
4331
4331
  return (status?.["api"])?.identity;
4332
4332
  }
4333
4333
  /**
4334
- * toPrivate removes people's and organizations' names from the status report. The keys that
4335
- * identify them stay, which is enough to find them without sending who they are.
4334
+ * The parts of an identity a report carries. Each identifies the credential, the person or
4335
+ * the organization without saying who they are. Anything the API adds later is left out
4336
+ * until someone decides it belongs here.
4337
+ */
4338
+ const IDENTITY_FIELDS = [
4339
+ "type",
4340
+ "identifier",
4341
+ "scopes",
4342
+ "createdAt",
4343
+ "expiresAt",
4344
+ "lastRegeneratedAt",
4345
+ "unavailable"
4346
+ ];
4347
+ /** The parts of a person or an organization a report carries, which are how Hardfin finds them. */
4348
+ const RECORD_FIELDS = ["id", "key"];
4349
+ /**
4350
+ * toPrivate keeps only what identifies who a credential is from the status report. A name
4351
+ * is left out, and so is a key's label, which is whatever a person typed when they made it.
4336
4352
  */
4337
4353
  function toPrivate(status) {
4338
- const identity = toIdentity(status);
4339
- if (!identity) return status;
4340
4354
  const api = status["api"];
4341
- const { organization, person } = identity;
4355
+ if (!isRecord(api) || !isRecord(api["identity"])) return status;
4356
+ const identity = api["identity"];
4342
4357
  return {
4343
4358
  ...status,
4344
4359
  api: {
4345
4360
  ...api,
4346
4361
  identity: {
4347
- ...api["identity"],
4348
- organization: organization ? {
4349
- ...organization,
4350
- name: void 0
4351
- } : void 0,
4352
- person: person ? {
4353
- ...person,
4354
- firstName: void 0,
4355
- lastName: void 0
4356
- } : void 0
4362
+ ...toPicked(identity, IDENTITY_FIELDS),
4363
+ organization: isRecord(identity["organization"]) ? toPicked(identity["organization"], RECORD_FIELDS) : void 0,
4364
+ person: isRecord(identity["person"]) ? toPicked(identity["person"], RECORD_FIELDS) : void 0
4357
4365
  }
4358
4366
  }
4359
4367
  };
4360
4368
  }
4369
+ function isRecord(value) {
4370
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4371
+ }
4372
+ /** toPicked copies the named fields of a record, and none of the others. */
4373
+ function toPicked(record, fields) {
4374
+ return Object.fromEntries(fields.filter((field) => field in record).map((field) => [field, record[field]]));
4375
+ }
4361
4376
  /** toTags picks the few values worth searching by, each with few distinct values. */
4362
4377
  function toTags(delivery) {
4363
4378
  const { occasion, status } = delivery;
@@ -4385,5 +4400,5 @@ function toTags(delivery) {
4385
4400
  //#endregion
4386
4401
  export { send };
4387
4402
 
4388
- //# sourceMappingURL=sentry-BM9Rvhhy.js.map
4389
- //# debugId=35c80804-dcd8-5662-9f00-9c8ff04f286a
4403
+ //# sourceMappingURL=sentry-CC0H3uMB.js.map
4404
+ //# debugId=8beb7849-4488-5315-88c8-ae70cca2939f
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardfin/cli",
3
- "version": "0.1.1-dev.44",
3
+ "version": "0.1.1-dev.46",
4
4
  "description": "Command line interface for the Hardfin API",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hardfin, Inc.",
@@ -29,7 +29,7 @@
29
29
  "templates"
30
30
  ],
31
31
  "engines": {
32
- "node": ">=20"
32
+ "node": ">=20.12"
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"
@@ -17,7 +17,7 @@ flags, so check `--help` before relying on a flag this skill does not show.
17
17
  hardfin --version || npm install -g @hardfin/cli
18
18
  ```
19
19
 
20
- It needs Node 20 or later. If `npm install -g` is refused, run commands as
20
+ It needs Node 20.12 or later. If `npm install -g` is refused, run commands as
21
21
  `npx -y @hardfin/cli <command>` instead.
22
22
 
23
23
  ## 2. Make sure it is signed in
@@ -28,11 +28,11 @@ hardfin status --offline --json
28
28
 
29
29
  `credential.kind` is `none` when nobody is signed in. Then sign in one of three ways.
30
30
 
31
- | Situation | Do this |
32
- | ---------------------------------------------------------- | ------------------------------------------------ |
33
- | You run on the person's own machine | Run `hardfin login` in the foreground |
34
- | You run in a cloud sandbox, away from the person's browser | Run `hardfin login --device` |
35
- | The person gave you an API key | Pass `--api-key <key>`, or set `HARDFIN_API_KEY` |
31
+ | Situation | Do this |
32
+ | ---------------------------------------------------------- | ---------------------------------------- |
33
+ | You run on the person's own machine | Run `hardfin login` in the foreground |
34
+ | You run in a cloud sandbox, away from the person's browser | Run `hardfin login --device` |
35
+ | The person gave you an API key | Set `HARDFIN_API_KEY` in the environment |
36
36
 
37
37
  `hardfin login` writes one JSON line on stderr before it waits:
38
38
 
@@ -44,7 +44,9 @@ hardfin status --offline --json
44
44
  It then waits up to `waitSeconds` (300) for the person to approve. Give the command that
45
45
  long to finish. Do not run it in the background and move on.
46
46
 
47
- Never write an API key into a file in the repository, and never echo it back.
47
+ Never write an API key into a file in the repository, and never echo it back. Never pass it
48
+ as `--api-key` either, because a command line is kept in transcripts and shown to other
49
+ programs on the machine.
48
50
 
49
51
  ## 3. Find the command
50
52
 
@@ -4,7 +4,7 @@ This project works with Hardfin data through the Hardfin CLI, `hardfin`.
4
4
 
5
5
  | Task | Command |
6
6
  | ---------------------------------- | ---------------------------------------------------------------------------- |
7
- | Install | `npm install -g @hardfin/cli`, with Node 20 or later |
7
+ | Install | `npm install -g @hardfin/cli`, with Node 20.12 or later |
8
8
  | Sign in | `hardfin login`, or `--device` away from a browser, or set `HARDFIN_API_KEY` |
9
9
  | Check what it is connected to | `hardfin status` |
10
10
  | Find a command | `hardfin --help`, then `hardfin <noun> --help` |