@hardfin/cli 0.1.0-dev.30 → 0.1.0-dev.31

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
@@ -159,8 +159,18 @@ hardfin status # what the CLI is configured with and signed in as
159
159
  hardfin logout # forget this machine, and ask Hardfin to revoke it
160
160
  ```
161
161
 
162
- An API key is still the way to run unattended. `HARDFIN_API_KEY` wins over a stored sign in,
163
- so CI and an agent sandbox need no browser.
162
+ An API key is still the way to run unattended. `--api-key`, or `HARDFIN_API_KEY`, wins over
163
+ a stored sign in, so CI and an agent sandbox need no browser. The flag beats the variable.
164
+
165
+ An API key is held to two rules before it is sent:
166
+
167
+ | Rule | Why |
168
+ | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
169
+ | It goes only over https, or to this machine | Anything between here and the host could read it |
170
+ | A key you set, by flag or exported variable, never goes to an API URL that `config.local.json` or `.env` in the working directory chose | A repository you cloned could otherwise send your key to its own server |
171
+
172
+ A key and an API URL that come from the same folder's files still work together, which is
173
+ how a local build is set up. Pass `--api-url` to send a key you set to that folder's address.
164
174
 
165
175
  ### What login does
166
176
 
@@ -373,7 +383,7 @@ settings, and the one nearest the top wins.
373
383
 
374
384
  | Layer | Where | Beats |
375
385
  | ----------- | --------------------------------------------------------------------- | ---------------- |
376
- | Flag | `--api-url` | everything below |
386
+ | Flag | `--api-url`, `--api-key`, `--issuer-url` | everything below |
377
387
  | Environment | an exported `HARDFIN_*` variable | the files below |
378
388
  | Env file | `.env` in the working directory, or the file `HARDFIN_ENV_FILE` names | the config file |
379
389
  | Config file | `config.local.json` in the working directory | the defaults |
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]="29560915-e7ae-5a74-b5f6-f4a6988e1b43")}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]="bb3026cb-ab06-5af0-9743-290b61ec91a3")}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-BZW_50HP.js");
17
+ const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-B0vISolG.js");
18
18
  try {
19
19
  await toCli().parseAsync(process.argv);
20
20
  } catch (error) {
@@ -33,7 +33,7 @@ async function toCrash(error) {
33
33
  isCrashing = true;
34
34
  process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
35
35
  try {
36
- const { toReported } = await import("./program-BZW_50HP.js");
36
+ const { toReported } = await import("./program-B0vISolG.js");
37
37
  await toReported(error, false, void 0, "fatal");
38
38
  } finally {
39
39
  process.exit(1);
@@ -51,4 +51,4 @@ async function toQuickAnswer(args) {
51
51
  export {};
52
52
 
53
53
  //# sourceMappingURL=cli.js.map
54
- //# debugId=29560915-e7ae-5a74-b5f6-f4a6988e1b43
54
+ //# debugId=bb3026cb-ab06-5af0-9743-290b61ec91a3
@@ -5,6 +5,11 @@
5
5
  "description": "The API to call, whose host also holds the authorization server",
6
6
  "valueName": "url"
7
7
  },
8
+ {
9
+ "name": "api-key",
10
+ "description": "An API key to call with, in place of HARDFIN_API_KEY or a sign in",
11
+ "valueName": "key"
12
+ },
8
13
  {
9
14
  "name": "issuer-url",
10
15
  "description": "The authorization server, when it does not sit at the API's host",
@@ -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]="5c189832-33ce-5032-b0e0-fc3a33cf5b1d")}catch(e){}}();
4
- import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-VtQmxap1.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]="ab584cf3-443f-569e-8167-fa0cbab9ab68")}catch(e){}}();
4
+ import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BN3gyBpR.js";
5
5
  import { n as toCompletion, t as toAnswer$1 } from "./completion-tree-rR_Rmeij.js";
6
6
  import { createRequire } from "node:module";
7
7
  import { Command, CommanderError, Option } from "commander";
@@ -694,12 +694,15 @@ const held = /* @__PURE__ */ new Map();
694
694
  * unattended caller sets it deliberately, and otherwise the stored refresh token buys an
695
695
  * access token that lives only in memory.
696
696
  */
697
- async function toRequestCredential(settings) {
698
- if (settings.apiKey) return {
699
- header: "X-API-Key",
700
- value: settings.apiKey,
701
- kind: "api key"
702
- };
697
+ async function toRequestCredential(settings, sources) {
698
+ if (settings.apiKey) {
699
+ refuseForeignKey(settings, sources);
700
+ return {
701
+ header: "X-API-Key",
702
+ value: settings.apiKey,
703
+ kind: "api key"
704
+ };
705
+ }
703
706
  refuseForeignHost(settings);
704
707
  const stored = toCredential(settings.issuerUrl);
705
708
  if (!stored) throw new NoCredential("not authenticated. Run hardfin login, or set HARDFIN_API_KEY");
@@ -775,6 +778,20 @@ function refuseForeignHost(settings) {
775
778
  if (api.host !== issuer.host) throw new NoCredential(`this sign in belongs to ${issuer.host}, so it is not sent to ${api.host}. Set HARDFIN_API_KEY to call another host, or point --issuer-url at the server that signs you in`);
776
779
  if (api.protocol !== "https:" && !isLoopback(api.hostname)) throw new NoCredential(`${settings.apiUrl} is not https, so a sign in is not sent to it`);
777
780
  }
781
+ /** The layers a file in the working directory sets, which a cloned repository can carry. */
782
+ const DIRECTORY_SOURCES = /* @__PURE__ */ new Set(["config file", "env file"]);
783
+ /** refuseForeignKey keeps an API key off plain connections and off hosts a stray file chose. */
784
+ function refuseForeignKey(settings, sources) {
785
+ const api = toHost$1(settings.apiUrl);
786
+ if (api === void 0) throw new NoCredential(`${settings.apiUrl} is not a URL this CLI can call`);
787
+ if (api.protocol !== "https:" && !isLoopback(api.hostname)) throw new NoCredential(`Your API key was not sent, because ${api.host} does not use a secure connection.`);
788
+ const isKeyChosen = sources?.apiKey !== void 0 && !DIRECTORY_SOURCES.has(sources.apiKey);
789
+ const isUrlFromFolder = sources?.apiUrl !== void 0 && DIRECTORY_SOURCES.has(sources.apiUrl);
790
+ if (isKeyChosen && isUrlFromFolder) {
791
+ const file = sources.apiUrl === "config file" ? CONFIG_FILE : ".env";
792
+ throw new NoCredential(`Your API key was not sent to ${api.host}, because that address comes from a file in this folder, not from you.\nDetails: the API URL is set in ${file}. Pass --api-url to use it anyway`);
793
+ }
794
+ }
778
795
  function toHost$1(url) {
779
796
  try {
780
797
  return new URL(url);
@@ -790,12 +807,15 @@ function isLoopback(hostname) {
790
807
  * an access token this process already holds. An error report uses it, because a refresh
791
808
  * cut short by the process ending would spend the refresh token and sign the person out.
792
809
  */
793
- function toHeldCredential(settings) {
794
- if (settings.apiKey) return {
795
- header: "X-API-Key",
796
- value: settings.apiKey,
797
- kind: "api key"
798
- };
810
+ function toHeldCredential(settings, sources) {
811
+ if (settings.apiKey) {
812
+ refuseForeignKey(settings, sources);
813
+ return {
814
+ header: "X-API-Key",
815
+ value: settings.apiKey,
816
+ kind: "api key"
817
+ };
818
+ }
799
819
  const current = held.get(settings.issuerUrl);
800
820
  if (!current || (current.expiresAt ?? 0) - EXPIRY_MARGIN_MS <= Date.now()) return;
801
821
  return {
@@ -966,7 +986,7 @@ function toGuide(commands, version) {
966
986
  "",
967
987
  "## Before calling",
968
988
  "",
969
- "- Authenticate by setting `HARDFIN_API_KEY`, or by running `hardfin login`",
989
+ "- Authenticate with `--api-key`, by setting `HARDFIN_API_KEY`, or by running `hardfin login`",
970
990
  "- Every command prints JSON on stdout and diagnostics on stderr",
971
991
  "- Output is JSON whenever stdout is not a terminal, so no flag is needed when calling from code",
972
992
  "",
@@ -1131,7 +1151,7 @@ async function runApi(input) {
1131
1151
  try {
1132
1152
  writeData((await request$1({
1133
1153
  apiUrl: input.resolved.settings.apiUrl,
1134
- credential: await toRequestCredential(input.resolved.settings),
1154
+ credential: await toRequestCredential(input.resolved.settings, input.resolved.sources),
1135
1155
  method: toText(input.flags["method"] ?? "GET").toUpperCase(),
1136
1156
  path,
1137
1157
  query,
@@ -2543,7 +2563,7 @@ function toServerReport(metadata) {
2543
2563
  async function toApiReport(resolved, options) {
2544
2564
  const settings = resolved.settings;
2545
2565
  try {
2546
- const credential = options.isHeldOnly ? toHeldCredential(settings) : await toRequestCredential(settings);
2566
+ const credential = options.isHeldOnly ? toHeldCredential(settings, resolved.sources) : await toRequestCredential(settings, resolved.sources);
2547
2567
  if (!credential) return {
2548
2568
  checked: false,
2549
2569
  url: settings.apiUrl,
@@ -2675,7 +2695,7 @@ async function report(error, occasion, resolved) {
2675
2695
  const settings = resolved ?? toResolved();
2676
2696
  const status = settings ? await toStatus(settings) : void 0;
2677
2697
  const secrets = [settings?.settings.apiKey, settings ? toCredential(settings.settings.issuerUrl)?.refreshToken : void 0];
2678
- return await (await import("./sentry-D5lKx0nm.js")).send({
2698
+ return await (await import("./sentry-CLaeAWNa.js")).send({
2679
2699
  error,
2680
2700
  occasion,
2681
2701
  telemetry,
@@ -2867,7 +2887,7 @@ const REFUSED_COMMANDS = /* @__PURE__ */ new Set([
2867
2887
  "mcp"
2868
2888
  ]);
2869
2889
  /** The global options, which sit before a command and take a value of their own. */
2870
- const GLOBAL_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--api-url", "--issuer-url"]);
2890
+ const GLOBAL_OPTIONS_WITH_VALUES = new Set(GLOBAL_FLAGS.filter((flag) => flag.valueName).map((flag) => `--${flag.name}`));
2871
2891
  /**
2872
2892
  * toCommandName reads which command a call names. A global option sits before it, so
2873
2893
  * reading the first argument alone would let --issuer-url x logout through.
@@ -3153,7 +3173,7 @@ async function runOperation(operation, input) {
3153
3173
  try {
3154
3174
  const envelope = await request$1({
3155
3175
  apiUrl: input.resolved.settings.apiUrl,
3156
- credential: await toRequestCredential(input.resolved.settings),
3176
+ credential: await toRequestCredential(input.resolved.settings, input.resolved.sources),
3157
3177
  method: operation.method,
3158
3178
  path,
3159
3179
  query: toQuery(operation, input.flags),
@@ -6060,6 +6080,7 @@ async function toExitCode(root, command, args, flags) {
6060
6080
  try {
6061
6081
  resolved = toSettings({
6062
6082
  apiUrl: root.opts()["apiUrl"],
6083
+ apiKey: root.opts()["apiKey"],
6063
6084
  issuerUrl: root.opts()["issuerUrl"]
6064
6085
  });
6065
6086
  return await command.run?.({
@@ -6101,5 +6122,5 @@ async function toReported(error, isJSON, resolved, level = "error") {
6101
6122
  //#endregion
6102
6123
  export { toCli, toParseExitCode, toReported, wasReportAttempted };
6103
6124
 
6104
- //# sourceMappingURL=program-BZW_50HP.js.map
6105
- //# debugId=5c189832-33ce-5032-b0e0-fc3a33cf5b1d
6125
+ //# sourceMappingURL=program-B0vISolG.js.map
6126
+ //# debugId=ab584cf3-443f-569e-8167-fa0cbab9ab68
@@ -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]="9fab3688-8d6c-599c-94b2-64daa75cd233")}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]="3af5060a-b6fe-5f2e-9661-01f4460a7b04")}catch(e){}}();
4
4
  import { z } from "zod";
5
5
  import { homedir } from "node:os";
6
6
  //#region src/command/registry.ts
@@ -17,17 +17,26 @@ function defineCommand(command) {
17
17
  return command;
18
18
  }
19
19
  /** GLOBAL_FLAGS are the options the CLI takes before any command. */
20
- const GLOBAL_FLAGS = [{
21
- name: "api-url",
22
- description: "The API to call, whose host also holds the authorization server",
23
- valueName: "url",
24
- schema: z.string()
25
- }, {
26
- name: "issuer-url",
27
- description: "The authorization server, when it does not sit at the API's host",
28
- valueName: "url",
29
- schema: z.string()
30
- }];
20
+ const GLOBAL_FLAGS = [
21
+ {
22
+ name: "api-url",
23
+ description: "The API to call, whose host also holds the authorization server",
24
+ valueName: "url",
25
+ schema: z.string()
26
+ },
27
+ {
28
+ name: "api-key",
29
+ description: "An API key to call with, in place of HARDFIN_API_KEY or a sign in",
30
+ valueName: "key",
31
+ schema: z.string()
32
+ },
33
+ {
34
+ name: "issuer-url",
35
+ description: "The authorization server, when it does not sit at the API's host",
36
+ valueName: "url",
37
+ schema: z.string()
38
+ }
39
+ ];
31
40
  //#endregion
32
41
  //#region src/report/scrub.ts
33
42
  const REDACTED = "[redacted]";
@@ -109,5 +118,5 @@ function toFlag(flags, word) {
109
118
  //#endregion
110
119
  export { defineCommand as a, GLOBAL_FLAGS as i, toScrubbed as n, ExitCode as r, toInvocation as t };
111
120
 
112
- //# sourceMappingURL=scrub-VtQmxap1.js.map
113
- //# debugId=9fab3688-8d6c-599c-94b2-64daa75cd233
121
+ //# sourceMappingURL=scrub-BN3gyBpR.js.map
122
+ //# debugId=3af5060a-b6fe-5f2e-9661-01f4460a7b04
@@ -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]="a251c173-a448-5d16-93ea-bc9547e82365")}catch(e){}}();
4
- import { n as toScrubbed } from "./scrub-VtQmxap1.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]="853940c3-9e72-566b-ad70-9f043e6aba8d")}catch(e){}}();
4
+ import { n as toScrubbed } from "./scrub-BN3gyBpR.js";
5
5
  import { dirname } from "node:path";
6
6
  import { request } from "node:http";
7
7
  import { fileURLToPath } from "node:url";
@@ -4385,5 +4385,5 @@ function toTags(delivery) {
4385
4385
  //#endregion
4386
4386
  export { send };
4387
4387
 
4388
- //# sourceMappingURL=sentry-D5lKx0nm.js.map
4389
- //# debugId=a251c173-a448-5d16-93ea-bc9547e82365
4388
+ //# sourceMappingURL=sentry-CLaeAWNa.js.map
4389
+ //# debugId=853940c3-9e72-566b-ad70-9f043e6aba8d
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardfin/cli",
3
- "version": "0.1.0-dev.30",
3
+ "version": "0.1.0-dev.31",
4
4
  "description": "Command line interface for the Hardfin API",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hardfin, Inc.",