@hardfin/cli 0.0.2-dev.7 → 0.0.2-dev.9

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 (3) hide show
  1. package/README.md +117 -14
  2. package/dist/cli.js +2139 -970
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -7,8 +7,8 @@ It is built for people at a terminal and for agents that call Hardfin on their b
7
7
 
8
8
  ## Status
9
9
 
10
- The CLI is early. `hardfin api` and the generated commands work against an API key, and
11
- `hardfin login` is not built yet.
10
+ The CLI is early. Signing in works against a Hardfin that publishes an authorization
11
+ server, and every command also accepts an API key.
12
12
 
13
13
  ## Install
14
14
 
@@ -31,6 +31,7 @@ Most commands are generated from the published API document rather than written
31
31
  | `hardfin api` | A call to any endpoint, written by hand, and the escape hatch when no generated command fits |
32
32
  | `src/command/surface.generated.ts` | Every endpoint as a command, rewritten by the generator |
33
33
  | `surface-overrides.json` | The operations whose generated name is wrong |
34
+ | `spec/core.openapi.yaml` | The API document the generator reads, vendored here |
34
35
  | `scripts/generate-surface.mjs` | The generator |
35
36
 
36
37
  ### How an endpoint becomes a command
@@ -70,20 +71,123 @@ Settle it in `surface-overrides.json`, keyed by `operationId`:
70
71
  An override for an `operationId` the document no longer publishes fails the generator. That
71
72
  is deliberate, because a silently dropped override renames a command nobody meant to rename.
72
73
 
73
- ### Regenerating
74
+ ### The vendored document
74
75
 
75
- The Surface workflow runs each weekday, reads `reference/core.openapi.yaml` from the
76
- `hardfinhq/api-spec` repository through a read-only deploy key, and opens a pull request when
77
- the generated file changes. It needs the `API_SPEC_READ_DEPLOY_KEY` secret.
76
+ `spec/core.openapi.yaml` is the API document this repository carries, and the generator
77
+ reads it. Nothing fetches a document during a build, a release, or CI.
78
78
 
79
- Run it by hand against a local document:
79
+ Refresh it with the script, which then rewrites the generated commands:
80
80
 
81
81
  ```sh
82
- npm run generate-surface -- ../api-spec/reference/core.openapi.yaml
82
+ scripts/update-spec.sh # reads hardfinhq/api-spec over your own git access
83
+ scripts/update-spec.sh ../hardfin # bundles the fragmented source in a monorepo checkout
83
84
  ```
84
85
 
85
- The document is bundled, meaning its external files are inlined, but `$ref` pointers within
86
- it remain. The generator follows those pointers itself.
86
+ The monorepo form runs the same bundler at the same version CI uses, so the result matches
87
+ what the api-spec bridge publishes. Commit the document and the generated commands together.
88
+
89
+ Two checks keep the pair honest.
90
+
91
+ | Check | Refuses |
92
+ | --- | --- |
93
+ | The generator | a document whose `info.version` is not a date, which means an earlier release |
94
+ | CI | a vendored document that was updated without regenerating the commands |
95
+
96
+ The first one matters because `hardfinhq/api-spec` can sit a release behind the monorepo
97
+ while its bridge pull request is open. Generating from that document would replace the
98
+ current commands with an earlier API's.
99
+
100
+ ## Signing in
101
+
102
+ ```sh
103
+ hardfin login # approve this CLI in a browser
104
+ hardfin status # what the CLI is configured with and signed in as
105
+ hardfin logout # forget this machine, and ask Hardfin to revoke it
106
+ ```
107
+
108
+ An API key is still the way to run unattended. `HARDFIN_API_KEY` wins over a stored sign in,
109
+ so CI and an agent sandbox need no browser.
110
+
111
+ ### What login does
112
+
113
+ 1. Reads `/.well-known/oauth-authorization-server` at the API's host, which names every
114
+ endpoint, including the authorize endpoint on the app host
115
+ 2. Opens a loopback listener on `127.0.0.1`, over IPv4, on whatever port the machine hands out
116
+ 3. Opens the browser to approve this CLI, with a PKCE challenge and a state value
117
+ 4. Prints the URL, and waits at the keyboard as well as on the listener
118
+ 5. Checks the state and the `iss` it came back with, then trades the code for tokens
119
+ 6. Keeps the refresh token, and nothing else
120
+
121
+ While it waits, the terminal takes two things.
122
+
123
+ | Key | Does |
124
+ | --- | --- |
125
+ | `c` | Copies the URL to the clipboard |
126
+ | A pasted value, then enter | Finishes the sign in without the listener |
127
+
128
+ Paste whichever of these you have: the whole redirect URL from the browser's address bar,
129
+ `code#state`, or the code alone. A browser on another machine never reaches this listener,
130
+ and the redirect it failed to follow is still in the address bar.
131
+
132
+ On WSL the browser is opened through Windows, by `wslview`, then PowerShell, then
133
+ `explorer.exe`, because `xdg-open` reaches nothing outside the distribution.
134
+
135
+ The port changes every time, so Hardfin matches a loopback redirect by everything except its
136
+ port, which is what RFC 8252 asks of an authorization server.
137
+
138
+ ### Where the refresh token is kept
139
+
140
+ | Host | Kept in |
141
+ | --- | --- |
142
+ | macOS, Windows, and Linux with a secret service | the OS keyring |
143
+ | Everything else, including WSL, containers, and agent sandboxes | `$XDG_STATE_HOME/hardfin/credentials.json`, mode 0600 |
144
+
145
+ `HARDFIN_NO_BROWSER=1` keeps `hardfin login` from opening a tab, which is what a script or a
146
+ headless host wants. `HARDFIN_CREDENTIAL_STORE=file` forces the file. `hardfin status` reports which one holds
147
+ the credential, and where.
148
+
149
+ Access tokens are never written anywhere. One is fetched when a command needs it and lives
150
+ in that process only, so what sits at rest is revocable with `hardfin logout`.
151
+
152
+ A credential is filed under the authorization server it came from, so a local build and
153
+ production never share a sign in.
154
+
155
+ ### What it needs configured
156
+
157
+ Nothing. Hardfin registers this CLI as a first-party client, and its key is the same in every
158
+ environment, so `hardfin login` works out of the box.
159
+
160
+ | Setting | Holds | Default |
161
+ | --- | --- | --- |
162
+ | `clientId`, or `HARDFIN_CLIENT_ID` | the client this CLI names itself as | the seeded Hardfin CLI client |
163
+ | `issuerUrl`, or `HARDFIN_ISSUER_URL` | the authorization server | the API's host root |
164
+
165
+ ## Diagnosing a problem
166
+
167
+ `hardfin status` prints everything a support request needs, and is the first thing to send.
168
+
169
+ ```sh
170
+ hardfin status # for you
171
+ hardfin status --json # for a support request
172
+ hardfin status --offline # no network calls at all
173
+ ```
174
+
175
+ | Section | Holds |
176
+ | --- | --- |
177
+ | `cli` | The version, the API version it speaks, Node, and whether a terminal is attached |
178
+ | `host` | The operating system, kernel, build, processor, memory, timezone, shell, and the distribution and WSL details on Linux |
179
+ | `configuration` | Every setting, its value, and the layer it came from |
180
+ | `credential` | Whether you are signed in, where the refresh token is kept, and when it was issued and last renewed |
181
+ | `authorizationServer` | Whether discovery worked, and every endpoint it named |
182
+ | `api` | Whether the API accepted the credential, when the access token expires, and its scopes |
183
+
184
+ Nothing secret is printed. An API key and a refresh token are each reported as a
185
+ `sha256:` fingerprint, which identifies a credential across two machines without disclosing
186
+ it.
187
+
188
+ `refreshExpiresAt` is an estimate. Hardfin expires an unused refresh token after 90 days,
189
+ but the token endpoint reports no expiry, so the CLI applies that rule to the date it last
190
+ renewed. The flag `refreshExpiryIsEstimated` says so in the output.
87
191
 
88
192
  ## Local configuration
89
193
 
@@ -105,13 +209,12 @@ An exported variable beats `.env` because Node leaves a variable that is already
105
209
  ```json
106
210
  {
107
211
  "apiUrl": "http://localhost:8080/v2",
108
- "auth": { "tokenUrl": "http://localhost:9000/oauth/token" }
212
+ "clientId": "https://hardfin.com/cli/client.json"
109
213
  }
110
214
  ```
111
215
 
112
- The authentication endpoints follow `apiUrl`, so pointing at a local server moves the whole
113
- flow. Name one under `auth` to move only that one. The keys are `apiUrl`, `apiKey`,
114
- `clientId`, and `auth` holding `authorizeUrl`, `tokenUrl`, `deviceUrl`, and `revokeUrl`.
216
+ The authorization server sits at the API's host, so pointing at a local server moves signing
217
+ in with it. The keys are `apiUrl`, `apiKey`, `clientId`, and `issuerUrl`.
115
218
 
116
219
  A key the file does not define fails the command with exit code 2. A typo that was silently
117
220
  ignored would look like a setting that never applied.